# frozen_string_literal: true

require 'spec_helper'

RSpec.shared_examples 'languages and percentages JSON response' do
  let(:expected_languages) { project.repository.languages.to_h { |language| language.values_at(:label, :value) } }

  before do
    allow(project.repository).to receive(:languages).and_return(
      [{ value: 66.69, label: "Ruby", color: "#701516", highlight: "#701516" },
       { value: 22.98, label: "JavaScript", color: "#f1e05a", highlight: "#f1e05a" },
       { value: 7.91, label: "HTML", color: "#e34c26", highlight: "#e34c26" },
       { value: 2.42, label: "CoffeeScript", color: "#244776", highlight: "#244776" }]
    )
  end

  context "when the languages haven't been detected yet" do
    it 'returns expected language values', :aggregate_failures, :sidekiq_might_not_need_inline do
      get api("/projects/#{project.id}/languages", user)

      expect(response).to have_gitlab_http_status(:ok)
      expect(json_response).to eq({})

      get api("/projects/#{project.id}/languages", user)

      expect(response).to have_gitlab_http_status(:ok)
      expect(Gitlab::Json.parse(response.body)).to eq(expected_languages)
    end
  end

  context 'when the languages were detected before' do
    before do
      Projects::DetectRepositoryLanguagesService.new(project, project.first_owner).execute
    end

    it 'returns the detection from the database', :aggregate_failures do
      # Allow this to happen once, so the expected languages can be determined
      expect(project.repository).to receive(:languages).once

      get api("/projects/#{project.id}/languages", user)

      expect(response).to have_gitlab_http_status(:ok)
      expect(json_response).to eq(expected_languages)
      expect(json_response.count).to be > 1
    end
  end
end

RSpec.describe API::Projects, :aggregate_failures, feature_category: :projects do
  include ProjectForksHelper
  include WorkhorseHelpers
  include StubRequests

  let_it_be(:user) { create(:user) }
  let_it_be(:user2) { create(:user) }
  let_it_be(:user3) { create(:user) }
  let_it_be(:admin) { create(:admin) }
  let_it_be(:project, reload: true) { create(:project, :repository, create_branch: 'something_else', namespace: user.namespace, updated_at: 5.days.ago) }
  let_it_be(:project2, reload: true) { create(:project, namespace: user.namespace, updated_at: 4.days.ago) }
  let_it_be(:project_member) { create(:project_member, :developer, user: user3, project: project) }
  let_it_be(:user4) { create(:user, username: 'user.withdot') }
  let_it_be(:project3, reload: true) do
    create(:project,
    :private,
    :repository,
    creator_id: user.id,
    namespace: user.namespace,
    merge_requests_enabled: false,
    issues_enabled: false, wiki_enabled: false,
    builds_enabled: false,
    snippets_enabled: false)
  end

  let_it_be(:project_member2) do
    create(:project_member,
    user: user4,
    project: project3,
    access_level: ProjectMember::MAINTAINER)
  end

  let_it_be(:project4, reload: true) do
    create(:project,
    creator_id: user4.id,
    namespace: user4.namespace)
  end

  let(:user_projects) { [public_project, project, project2, project3] }

  shared_context 'with language detection' do
    let(:ruby) { create(:programming_language, name: 'Ruby') }
    let(:javascript) { create(:programming_language, name: 'JavaScript') }
    let(:html) { create(:programming_language, name: 'HTML') }

    let(:mock_repo_languages) do
      {
        project => { ruby => 0.5, html => 0.5 },
        project3 => { html => 0.7, javascript => 0.3 }
      }
    end

    before do
      mock_repo_languages.each do |proj, lang_shares|
        lang_shares.each do |lang, share|
          create(:repository_language, project: proj, programming_language: lang, share: share)
        end
      end
    end
  end

  shared_examples_for 'create project with default branch parameter' do
    let(:params) { { name: 'Foo Project', initialize_with_readme: true, default_branch: default_branch } }
    let(:default_branch) { 'main' }

    it 'creates project with provided default branch name' do
      expect { request }.to change { Project.count }.by(1)
      expect(response).to have_gitlab_http_status(:created)

      project = Project.find(json_response['id'])
      expect(project.default_branch).to eq(default_branch)
    end

    context 'when branch name is empty' do
      let(:default_branch) { '' }

      it 'creates project with a default project branch name' do
        expect { request }.to change { Project.count }.by(1)
        expect(response).to have_gitlab_http_status(:created)

        project = Project.find(json_response['id'])
        expect(project.default_branch).to eq('master')
      end
    end

    context 'when initialize with readme is not set' do
      let(:params) { super().merge(initialize_with_readme: nil) }

      it 'creates project with a default project branch name' do
        expect { request }.to change { Project.count }.by(1)
        expect(response).to have_gitlab_http_status(:created)

        project = Project.find(json_response['id'])
        expect(project.default_branch).to be_nil
      end
    end
  end

  describe 'GET /projects' do
    let(:path) { '/projects' }

    let_it_be(:public_project) { create(:project, :public, name: 'public_project') }

    shared_examples_for 'projects response' do
      let_it_be(:admin_mode) { false }

      it 'returns an array of projects' do
        get api(path, current_user, admin_mode: admin_mode), params: filter

        expect(response).to have_gitlab_http_status(:ok)
        expect(response).to include_pagination_headers
        expect(json_response).to be_an Array
        expect(json_response.map { |p| p['id'] }).to contain_exactly(*projects.map(&:id))
      end

      it 'returns the proper security headers' do
        get api(path, current_user, admin_mode: admin_mode), params: filter

        expect(response).to include_security_headers
      end
    end

    shared_examples_for 'projects response without N + 1 queries' do |threshold|
      let(:additional_project) { create(:project, :public) }

      it 'avoids N + 1 queries', :use_sql_query_cache do
        control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
          get api(path, current_user)
        end

        additional_project

        expect do
          get api(path, current_user)
        end.not_to exceed_all_query_limit(control).with_threshold(threshold)
      end
    end

    context 'when unauthenticated' do
      it_behaves_like 'projects response' do
        let(:filter) { { search: project.path } }
        let(:current_user) { user }
        let(:projects) { [project] }
      end

      it_behaves_like 'projects response without N + 1 queries', 1 do
        let(:current_user) { nil }
      end
    end

    context 'when authenticated as regular user' do
      it_behaves_like 'projects response' do
        let(:filter) { {} }
        let(:current_user) { user }
        let(:projects) { user_projects }
      end

      it_behaves_like 'projects response without N + 1 queries', 0 do
        let(:current_user) { user }
      end

      shared_examples 'includes container_registry_access_level' do
        specify do
          project.project_feature.update!(container_registry_access_level: ProjectFeature::DISABLED)

          get api(path, user)
          project_response = json_response.find { |p| p['id'] == project.id }

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response).to be_an Array
          expect(project_response['container_registry_access_level']).to eq('disabled')
          expect(project_response['container_registry_enabled']).to eq(false)
        end
      end

      include_examples 'includes container_registry_access_level'

      context 'when projects_preloader_fix is disabled' do
        before do
          stub_feature_flags(projects_preloader_fix: false)
        end

        include_examples 'includes container_registry_access_level'
      end

      it 'includes various project feature fields' do
        get api(path, user)
        project_response = json_response.find { |p| p['id'] == project.id }

        expect(response).to have_gitlab_http_status(:ok)
        expect(project_response['releases_access_level']).to eq('enabled')
        expect(project_response['environments_access_level']).to eq('enabled')
        expect(project_response['feature_flags_access_level']).to eq('enabled')
        expect(project_response['infrastructure_access_level']).to eq('enabled')
        expect(project_response['monitor_access_level']).to eq('enabled')
      end

      context 'when some projects are in a group' do
        before do
          create(:project, :public, group: create(:group))
        end

        it_behaves_like 'projects response without N + 1 queries', 1 do
          let(:current_user) { user }
          let(:additional_project) { create(:project, :public, group: create(:group)) }
        end
      end

      it 'includes correct value of container_registry_enabled' do
        project.project_feature.update!(container_registry_access_level: ProjectFeature::DISABLED)

        get api(path, user)
        project_response = json_response.find { |p| p['id'] == project.id }

        expect(response).to have_gitlab_http_status(:ok)
        expect(json_response).to be_an Array
        expect(project_response['container_registry_enabled']).to eq(false)
      end

      it 'includes project topics' do
        get api(path, user)

        expect(response).to have_gitlab_http_status(:ok)
        expect(response).to include_pagination_headers
        expect(json_response).to be_an Array
        expect(json_response.first.keys).to include('tag_list') # deprecated in favor of 'topics'
        expect(json_response.first.keys).to include('topics')
      end

      it 'includes open_issues_count' do
        get api(path, user)

        expect(response).to have_gitlab_http_status(:ok)
        expect(response).to include_pagination_headers
        expect(json_response).to be_an Array
        expect(json_response.first.keys).to include('open_issues_count')
      end

      it 'does not include projects marked for deletion' do
        project.update!(pending_delete: true)

        get api(path, user)

        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(project.id)
      end

      it 'does not include open_issues_count if issues are disabled' do
        project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)

        get api(path, user)

        expect(response).to have_gitlab_http_status(:ok)
        expect(response).to include_pagination_headers
        expect(json_response).to be_an Array
        expect(json_response.find { |hash| hash['id'] == project.id }.keys).not_to include('open_issues_count')
      end

      context 'filter by topic (column topic_list)' do
        before do
          project.update!(topic_list: %w(ruby javascript))
        end

        it 'returns no projects' do
          get api(path, user), params: { topic: 'foo' }

          expect(response).to have_gitlab_http_status(:ok)
          expect(response).to include_pagination_headers
          expect(json_response).to be_empty
        end

        it 'returns matching project for a single topic' do
          get api(path, user), params: { topic: 'ruby' }

          expect(response).to have_gitlab_http_status(:ok)
          expect(response).to include_pagination_headers
          expect(json_response).to contain_exactly a_hash_including('id' => project.id)
        end

        it 'returns matching project for multiple topics' do
          get api(path, user), params: { topic: 'ruby, javascript' }

          expect(response).to have_gitlab_http_status(:ok)
          expect(response).to include_pagination_headers
          expect(json_response).to contain_exactly a_hash_including('id' => project.id)
        end

        it 'returns no projects if project match only some topic' do
          get api(path, user), params: { topic: 'ruby, foo' }

          expect(response).to have_gitlab_http_status(:ok)
          expect(response).to include_pagination_headers
          expect(json_response).to be_empty
        end

        it 'ignores topic if it is empty' do
          get api(path, user), params: { topic: '' }

          expect(response).to have_gitlab_http_status(:ok)
          expect(response).to include_pagination_headers
          expect(json_response).to be_present
        end
      end

      context 'filter by topic_id' do
        let_it_be(:topic1) { create(:topic) }
        let_it_be(:topic2) { create(:topic) }

        let(:current_user) { user }

        before do
          project.topics << topic1
        end

        context 'with id of assigned topic' do
          it_behaves_like 'projects response' do
            let(:filter) { { topic_id: topic1.id } }
            let(:projects) { [project] }
          end
        end

        context 'with id of unassigned topic' do
          it_behaves_like 'projects response' do
            let(:filter) { { topic_id: topic2.id } }
            let(:projects) { [] }
          end
        end

        context 'with non-existing topic id' do
          it_behaves_like 'projects response' do
            let(:filter) { { topic_id: non_existing_record_id } }
            let(:projects) { [] }
          end
        end

        context 'with empty topic id' do
          it_behaves_like 'projects response' do
            let(:filter) { { topic_id: '' } }
            let(:projects) { user_projects }
          end
        end
      end

      context 'and with_issues_enabled=true' do
        it 'only returns projects with issues enabled' do
          project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)

          get api('/projects?with_issues_enabled=true', user)

          expect(response).to have_gitlab_http_status(:ok)
          expect(response).to include_pagination_headers
          expect(json_response).to be_an Array
          expect(json_response.map { |p| p['id'] }).not_to include(project.id)
        end
      end

      it "does not include statistics by default" do
        get api(path, user)

        expect(response).to have_gitlab_http_status(:ok)
        expect(response).to include_pagination_headers
        expect(json_response).to be_an Array
        expect(json_response.first).not_to include('statistics')
      end

      it "includes statistics if requested" do
        get api(path, user), params: { statistics: true }

        expect(response).to have_gitlab_http_status(:ok)
        expect(response).to include_pagination_headers
        expect(json_response).to be_an Array

        statistics = json_response.find { |p| p['id'] == project.id }['statistics']
        expect(statistics).to be_present
        expect(statistics).to include('commit_count', 'storage_size', 'repository_size', 'wiki_size', 'lfs_objects_size', 'job_artifacts_size', 'pipeline_artifacts_size', 'snippets_size', 'packages_size', 'uploads_size')
      end

      it "does not include license by default" do
        get api(path, user)

        expect(response).to have_gitlab_http_status(:ok)
        expect(response).to include_pagination_headers
        expect(json_response).to be_an Array
        expect(json_response.first).not_to include('license', 'license_url')
      end

      it "does not include license if requested" do
        get api(path, user), params: { license: true }

        expect(response).to have_gitlab_http_status(:ok)
        expect(response).to include_pagination_headers
        expect(json_response).to be_an Array
        expect(json_response.first).not_to include('license', 'license_url')
      end

      context 'when external issue tracker is enabled' do
        let!(:jira_integration) { create(:jira_integration, project: project) }

        it 'includes open_issues_count' do
          get api(path, user)

          expect(response).to have_gitlab_http_status(:ok)
          expect(response).to include_pagination_headers
          expect(json_response).to be_an Array
          expect(json_response.first.keys).to include('open_issues_count')
          expect(json_response.find { |hash| hash['id'] == project.id }.keys).to include('open_issues_count')
        end

        it 'does not include open_issues_count if issues are disabled' do
          project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)

          get api(path, user)

          expect(response).to have_gitlab_http_status(:ok)
          expect(response).to include_pagination_headers
          expect(json_response).to be_an Array
          expect(json_response.find { |hash| hash['id'] == project.id }.keys).not_to include('open_issues_count')
        end
      end

      context 'and with simple=true' do
        it 'returns a simplified version of all the projects' do
          get api('/projects?simple=true', user)

          expect(response).to have_gitlab_http_status(:ok)
          expect(response).to include_pagination_headers
          expect(response).to match_response_schema('public_api/v4/projects')
        end
      end

      context 'and using archived' do
        let!(:archived_project) { create(:project, creator_id: user.id, namespace: user.namespace, archived: true) }

        it 'returns archived projects' do
          get api('/projects?archived=true', user)

          expect(response).to have_gitlab_http_status(:ok)
          expect(response).to include_pagination_headers
          expect(json_response).to be_an Array
          expect(json_response.length).to eq(Project.public_or_visible_to_user(user).where(archived: true).size)
          expect(json_response.map { |project| project['id'] }).to include(archived_project.id)
        end

        it 'returns non-archived projects' do
          get api('/projects?archived=false', user)

          expect(response).to have_gitlab_http_status(:ok)
          expect(response).to include_pagination_headers
          expect(json_response).to be_an Array
          expect(json_response.length).to eq(Project.public_or_visible_to_user(user).where(archived: false).size)
          expect(json_response.map { |project| project['id'] }).not_to include(archived_project.id)
        end

        it 'returns every project' do
          get api(path, user)

          expect(response).to have_gitlab_http_status(:ok)
          expect(response).to include_pagination_headers
          expect(json_response).to be_an Array
          expect(json_response.map { |project| project['id'] }).to contain_exactly(*Project.public_or_visible_to_user(user).pluck(:id))
        end
      end

      context 'filter by updated_at' do
        let(:filter) { { updated_before: 2.days.ago.iso8601, updated_after: 6.days.ago, order_by: :updated_at } }

        it_behaves_like 'projects response' do
          let(:current_user) { user }
          let(:projects) { [project2, project] }
        end

        it 'returns projects sorted by updated_at' do
          get api(path, user), params: filter

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response.map { |p| p['id'] }).to match([project2, project].map(&:id))
        end

        context 'when filtering by updated_at and sorting by a different column' do
          let(:filter) { { updated_before: 2.days.ago.iso8601, updated_after: 6.days.ago, order_by: 'id' } }

          it 'returns an error' do
            get api(path, user), params: filter

            expect(response).to have_gitlab_http_status(:bad_request)
            expect(json_response['message']).to eq(
              '400 Bad request - `updated_at` filter and `updated_at` sorting must be paired'
            )
          end
        end
      end

      context 'and using search' do
        it_behaves_like 'projects response' do
          let(:filter) { { search: project.path } }
          let(:current_user) { user }
          let(:projects) { [project] }
        end
      end

      context 'and using search and search_namespaces is true' do
        let(:group) { create(:group) }
        let!(:project_in_group) { create(:project, group: group) }

        before do
          group.add_guest(user)
        end

        it_behaves_like 'projects response' do
          let(:filter) { { search: group.name, search_namespaces: true } }
          let(:current_user) { user }
          let(:projects) { [project_in_group] }
        end
      end

      context 'and using id_after' do
        it_behaves_like 'projects response' do
          let(:filter) { { id_after: project2.id } }
          let(:current_user) { user }
          let(:projects) { user_projects.select { |p| p.id > project2.id } }
        end

        context 'regression: empty string is ignored' do
          it_behaves_like 'projects response' do
            let(:filter) { { id_after: '' } }
            let(:current_user) { user }
            let(:projects) { user_projects }
          end
        end
      end

      context 'and using id_before' do
        it_behaves_like 'projects response' do
          let(:filter) { { id_before: project2.id } }
          let(:current_user) { user }
          let(:projects) { user_projects.select { |p| p.id < project2.id } }
        end

        context 'regression: empty string is ignored' do
          it_behaves_like 'projects response' do
            let(:filter) { { id_before: '' } }
            let(:current_user) { user }
            let(:projects) { user_projects }
          end
        end
      end

      context 'and using both id_after and id_before' do
        it_behaves_like 'projects response' do
          let(:filter) { { id_before: project2.id, id_after: public_project.id } }
          let(:current_user) { user }
          let(:projects) { user_projects.select { |p| p.id < project2.id && p.id > public_project.id } }
        end
      end

      context 'and membership=true' do
        it_behaves_like 'projects response' do
          let(:filter) { { membership: true } }
          let(:current_user) { user }
          let(:projects) { [project, project2, project3] }
        end
      end

      context 'and using the visibility filter' do
        it 'filters based on private visibility param' do
          get api(path, user), params: { visibility: 'private' }

          expect(response).to have_gitlab_http_status(:ok)
          expect(response).to include_pagination_headers
          expect(json_response).to be_an Array
          expect(json_response.map { |p| p['id'] }).to contain_exactly(project.id, project2.id, project3.id)
        end

        it 'filters based on internal visibility param' do
          project2.update_attribute(:visibility_level, Gitlab::VisibilityLevel::INTERNAL)

          get api(path, user), params: { visibility: 'internal' }

          expect(response).to have_gitlab_http_status(:ok)
          expect(response).to include_pagination_headers
          expect(json_response).to be_an Array
          expect(json_response.map { |p| p['id'] }).to contain_exactly(project2.id)
        end

        it 'filters based on public visibility param' do
          get api(path, user), params: { visibility: 'public' }

          expect(response).to have_gitlab_http_status(:ok)
          expect(response).to include_pagination_headers
          expect(json_response).to be_an Array
          expect(json_response.map { |p| p['id'] }).to contain_exactly(public_project.id)
        end
      end

      context 'and using the programming language filter' do
        include_context 'with language detection'

        it 'filters case-insensitively by programming language' do
          get api(path, user), params: { with_programming_language: 'javascript' }

          expect(response).to have_gitlab_http_status(:ok)
          expect(response).to include_pagination_headers
          expect(json_response).to be_an Array
          expect(json_response.map { |p| p['id'] }).to contain_exactly(project3.id)
        end
      end

      context 'and using sorting' do
        it 'returns the correct order when sorted by id' do
          get api(path, user), params: { order_by: 'id', sort: 'desc' }

          expect(response).to have_gitlab_http_status(:ok)
          expect(response).to include_pagination_headers
          expect(json_response).to be_an Array
          expect(json_response.map { |p| p['id'] }).to eq(user_projects.map(&:id).sort.reverse)
        end
      end

      context 'and with owned=true' do
        it 'returns an array of projects the user owns' do
          get api(path, user4), params: { owned: true }

          expect(response).to have_gitlab_http_status(:ok)
          expect(response).to include_pagination_headers
          expect(json_response).to be_an Array
          expect(json_response.first['name']).to eq(project4.name)
          expect(json_response.first['owner']['username']).to eq(user4.username)
        end

        context 'when admin creates a project' do
          before do
            group = create(:group)
            project_create_opts = {
              name: 'GitLab',
              namespace_id: group.id
            }

            Projects::CreateService.new(admin, project_create_opts).execute
          end

          it 'does not list as owned project for admin' do
            get api(path, admin, admin_mode: true), params: { owned: true }

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

      context 'and with starred=true' do
        let(:public_project) { create(:project, :public) }

        before do
          user3.update!(starred_projects: [project, project2, project3, public_project])
        end

        it 'returns the starred projects viewable by the user' do
          get api(path, user3), params: { starred: true }

          expect(response).to have_gitlab_http_status(:ok)
          expect(response).to include_pagination_headers
          expect(json_response).to be_an Array
          expect(json_response.map { |project| project['id'] }).to contain_exactly(project.id, public_project.id)
        end
      end

      context 'and with all query parameters' do
        let!(:project5) { create(:project, :public, path: 'gitlab5', namespace: create(:namespace)) }
        let!(:project6) { create(:project, :public, namespace: user.namespace) }
        let!(:project7) { create(:project, :public, path: 'gitlab7', namespace: user.namespace) }
        let!(:project8) { create(:project, path: 'gitlab8', namespace: user.namespace) }
        let!(:project9) { create(:project, :public, path: 'gitlab9') }

        before do
          user.update!(starred_projects: [project5, project7, project8, project9])
        end

        context 'including owned filter' do
          it 'returns only projects that satisfy all query parameters' do
            get api(path, user), params: { visibility: 'public', owned: true, starred: true, search: 'gitlab' }

            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)
            expect(json_response.first['id']).to eq(project7.id)
          end
        end

        context 'including membership filter' do
          before do
            create(:project_member,
                   user: user,
                   project: project5,
                   access_level: ProjectMember::MAINTAINER)
          end

          it 'returns only projects that satisfy all query parameters' do
            get api(path, user), params: { visibility: 'public', membership: true, starred: true, search: 'gitlab' }

            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(2)
            expect(json_response.map { |project| project['id'] }).to contain_exactly(project5.id, project7.id)
          end
        end
      end

      context 'and with min_access_level' do
        before do
          project2.add_maintainer(user2)
          project3.add_developer(user2)
          project4.add_reporter(user2)
        end

        it 'returns an array of projects the user has at least developer access' do
          get api(path, user2), params: { min_access_level: 30 }

          expect(response).to have_gitlab_http_status(:ok)
          expect(response).to include_pagination_headers
          expect(json_response).to be_an Array
          expect(json_response.map { |project| project['id'] }).to contain_exactly(project2.id, project3.id)
        end
      end
    end

    context 'and imported=true' do
      before do
        other_user = create(:user)
        # imported project by other user
        create(:project, creator: other_user, import_type: 'github', import_url: 'http://foo.com')
        # project created by current user directly instead of importing
        create(:project)
        project.update_attribute(:import_url, 'http://user:password@host/path')
        project.update_attribute(:import_type, 'github')
      end

      it 'returns only imported projects owned by current user' do
        get api('/projects?imported=true', user)

        expect(response).to have_gitlab_http_status(:ok)
        expect(response).to include_pagination_headers
        expect(json_response).to be_an Array
        expect(json_response.map { |p| p['id'] }).to eq [project.id]
      end

      it 'does not expose import credentials' do
        get api('/projects?imported=true', user)

        expect(json_response.first['import_url']).to eq 'http://host/path'
      end
    end

    context 'when authenticated as a different user' do
      it_behaves_like 'projects response' do
        let(:filter) { {} }
        let(:current_user) { user2 }
        let(:projects) { [public_project] }
      end

      context 'and with_issues_enabled=true' do
        it 'does not return private issue projects' do
          project.project_feature.update_attribute(:issues_access_level, ProjectFeature::PRIVATE)

          get api('/projects?with_issues_enabled=true', user2)

          expect(response).to have_gitlab_http_status(:ok)
          expect(response).to include_pagination_headers
          expect(json_response).to be_an Array
          expect(json_response.map { |p| p['id'] }).not_to include(project.id)
        end
      end
    end

    context 'when authenticated as admin' do
      it_behaves_like 'projects response' do
        let(:filter) { {} }
        let(:current_user) { admin }
        let(:admin_mode) { true }
        let(:projects) { Project.all }
      end
    end

    context 'with default created_at desc order' do
      let_it_be(:group_with_projects) { create(:group) }
      let_it_be(:project_1) { create(:project, name: 'Project 1', created_at: 3.days.ago, path: 'project1', group: group_with_projects) }
      let_it_be(:project_2) { create(:project, name: 'Project 2', created_at: 2.days.ago, path: 'project2', group: group_with_projects) }
      let_it_be(:project_3) { create(:project, name: 'Project 3', created_at: 1.day.ago, path: 'project3', group: group_with_projects) }

      let(:current_user) { user }
      let(:params) { {} }

      subject(:request) { get api(path, current_user), params: params }

      before do
        group_with_projects.add_owner(current_user) if current_user
      end

      it 'orders by id desc instead' do
        projects_ordered_by_id_desc = /SELECT "projects".+ORDER BY "projects"."id" DESC/i
        expect { request }.to make_queries_matching projects_ordered_by_id_desc

        expect(response).to have_gitlab_http_status(:ok)
        expect(response).to include_pagination_headers
        expect(json_response).to be_an Array
        expect(json_response.first['id']).to eq(project_3.id)
      end
    end

    context 'sorting' do
      context 'by project statistics' do
        %w(repository_size storage_size wiki_size packages_size).each do |order_by|
          context "sorting by #{order_by}" do
            before do
              ProjectStatistics.update_all(order_by => 100)
              project4.statistics.update_columns(order_by => 10)
              project.statistics.update_columns(order_by => 200)
            end

            context 'admin user' do
              let(:current_user) { admin }

              context "when sorting by #{order_by} ascendingly" do
                it 'returns a properly sorted list of projects' do
                  get api(path, current_user, admin_mode: true), params: { order_by: order_by, sort: :asc }

                  expect(response).to have_gitlab_http_status(:ok)
                  expect(response).to include_pagination_headers
                  expect(json_response).to be_an Array
                  expect(json_response.first['id']).to eq(project4.id)
                end
              end

              context "when sorting by #{order_by} descendingly" do
                it 'returns a properly sorted list of projects' do
                  get api(path, current_user, admin_mode: true), params: { order_by: order_by, sort: :desc }

                  expect(response).to have_gitlab_http_status(:ok)
                  expect(response).to include_pagination_headers
                  expect(json_response).to be_an Array
                  expect(json_response.first['id']).to eq(project.id)
                end
              end
            end

            context 'non-admin user' do
              let(:current_user) { user }

              it 'returns projects ordered normally' do
                get api(path, current_user), params: { order_by: order_by }

                expect(response).to have_gitlab_http_status(:ok)
                expect(response).to include_pagination_headers
                expect(json_response).to be_an Array
                expect(json_response.map { |project| project['id'] }).to eq(user_projects.map(&:id).sort.reverse)
              end
            end
          end
        end
      end

      context 'by similarity' do
        let_it_be(:group_with_projects) { create(:group) }
        let_it_be(:project_1) { create(:project, name: 'Project', path: 'project', group: group_with_projects) }
        let_it_be(:project_2) { create(:project, name: 'Test Project', path: 'test-project', group: group_with_projects) }
        let_it_be(:project_3) { create(:project, name: 'Test', path: 'test', group: group_with_projects) }
        let_it_be(:project_4) { create(:project, :public, name: 'Test Public Project') }

        let(:current_user) { user }
        let(:params) { { order_by: 'similarity', search: 'test' } }

        subject(:request) { get api(path, current_user), params: params }

        before do
          group_with_projects.add_owner(current_user) if current_user
        end

        it 'returns non-public items based ordered by similarity' do
          request

          expect(response).to have_gitlab_http_status(:ok)
          expect(response).to include_pagination_headers
          expect(json_response.length).to eq(2)

          project_names = json_response.map { |proj| proj['name'] }
          expect(project_names).to contain_exactly('Test', 'Test Project')
        end

        context 'when `search` parameter is not given' do
          let(:params) { { order_by: 'similarity' } }

          it 'returns items ordered by created_at descending' do
            request

            expect(response).to have_gitlab_http_status(:ok)
            expect(response).to include_pagination_headers
            expect(json_response.length).to eq(8)

            project_names = json_response.map { |proj| proj['name'] }
            expect(project_names).to match_array([project, project2, project3, public_project, project_1, project_2, project_4, project_3].map(&:name))
          end
        end

        context 'when called anonymously' do
          let(:current_user) { nil }

          it 'returns items ordered by created_at descending' do
            request

            expect(response).to have_gitlab_http_status(:ok)
            expect(response).to include_pagination_headers
            expect(json_response.length).to eq(1)

            project_names = json_response.map { |proj| proj['name'] }
            expect(project_names).to contain_exactly(project_4.name)
          end
        end
      end
    end

    context 'filtering by repository_storage' do
      before do
        [project, project3].each { |proj| proj.update_columns(repository_storage: 'nfs-11') }
        # Since we don't actually have Gitaly configured with an nfs-11 storage, an error would be raised
        # when we present the projects in a response, as we ask Gitaly for stuff like default branch and Gitaly
        # is not configured for a nfs-11 storage. So we trick Rails into thinking the storage for these projects
        # is still default (in reality, it is).
        allow_any_instance_of(Project).to receive(:repository_storage).and_return('default')
      end

      context 'admin user' do
        it_behaves_like 'projects response' do
          let(:filter) { { repository_storage: 'nfs-11' } }
          let(:current_user) { admin }
          let(:admin_mode) { true }
          let(:projects) { [project, project3] }
        end
      end

      context 'non-admin user' do
        it_behaves_like 'projects response' do
          let(:filter) { { repository_storage: 'nfs-11' } }
          let(:current_user) { user }
          let(:projects) { [public_project, project, project2, project3] }
        end
      end
    end

    context 'with keyset pagination' do
      let(:current_user) { user }
      let(:first_project_id) { user_projects.map(&:id).min }
      let(:last_project_id) { user_projects.map(&:id).max }

      context 'headers and records' do
        let(:params) { { pagination: 'keyset', order_by: :id, sort: :asc, per_page: 1 } }

        it 'includes a pagination header with link to the next page' do
          get api(path, current_user), params: params

          expect(response.header).to include('Link')
          expect(response.header['Link']).to include('pagination=keyset')
          expect(response.header['Link']).to include("id_after=#{first_project_id}")
        end

        it 'contains only the first project with per_page = 1' do
          get api(path, current_user), params: params

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response).to be_an Array
          expect(json_response.map { |p| p['id'] }).to contain_exactly(first_project_id)
        end

        it 'still includes a link if the end has reached and there is no more data after this page' do
          get api(path, current_user), params: params.merge(id_after: project2.id)

          expect(response.header).to include('Link')
          expect(response.header['Link']).to include('pagination=keyset')
          expect(response.header['Link']).to include("id_after=#{project3.id}")
        end

        it 'does not include a next link when the page does not have any records' do
          get api(path, current_user), params: params.merge(id_after: Project.maximum(:id))

          expect(response.header).not_to include('Link')
        end

        it 'returns an empty array when the page does not have any records' do
          get api(path, current_user), params: params.merge(id_after: Project.maximum(:id))

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response).to eq([])
        end

        it 'responds with 501 if order_by is different from id' do
          get api(path, current_user), params: params.merge(order_by: :created_at)

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

      context 'with descending sorting' do
        let(:params) { { pagination: 'keyset', order_by: :id, sort: :desc, per_page: 1 } }

        it 'includes a pagination header with link to the next page' do
          get api(path, current_user), params: params

          expect(response.header).to include('Link')
          expect(response.header['Link']).to include('pagination=keyset')
          expect(response.header['Link']).to include("id_before=#{last_project_id}")
        end

        it 'contains only the last project with per_page = 1' do
          get api(path, current_user), params: params

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response).to be_an Array
          expect(json_response.map { |p| p['id'] }).to contain_exactly(last_project_id)
        end
      end

      context 'retrieving the full relation' do
        let(:params) { { pagination: 'keyset', order_by: :id, sort: :desc, per_page: 2 } }

        it 'returns all projects' do
          url = path
          requests = 0
          ids = []

          while url && requests <= 5 # circuit breaker
            requests += 1
            get api(url, current_user), params: params

            link = response.header['Link']
            url = link&.match(%r{<[^>]+(/projects\?[^>]+)>; rel="next"}) do |match|
              match[1]
            end

            ids += Gitlab::Json.parse(response.body).map { |p| p['id'] }
          end

          expect(ids).to contain_exactly(*user_projects.map(&:id))
        end
      end
    end

    context 'with forked projects', :use_clean_rails_memory_store_caching do
      include ProjectForksHelper

      let_it_be(:admin) { create(:admin) }

      subject(:request) { get api(path, admin) }

      it 'avoids N+1 queries', :use_sql_query_cache do
        request
        expect(response).to have_gitlab_http_status(:ok)

        base_project = create(:project, :public, namespace: admin.namespace)

        fork_project1 = fork_project(base_project, admin, namespace: create(:user).namespace)
        fork_project2 = fork_project(fork_project1, admin, namespace: create(:user).namespace)

        control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
          request
        end

        fork_project(fork_project2, admin, namespace: create(:user).namespace)

        expect do
          request
        end.not_to exceed_all_query_limit(control.count)
      end
    end

    context 'when service desk is enabled', :use_clean_rails_memory_store_caching do
      let_it_be(:admin) { create(:admin) }

      subject(:request) { get api(path, admin) }

      it 'avoids N+1 queries' do
        allow(Gitlab::Email::ServiceDeskEmail).to receive(:enabled?).and_return(true)
        allow(Gitlab::Email::IncomingEmail).to receive(:enabled?).and_return(true)

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

        create(:project, :public, :service_desk_enabled, namespace: admin.namespace)

        control = ActiveRecord::QueryRecorder.new do
          request
        end

        create_list(:project, 2, :public, :service_desk_enabled, namespace: admin.namespace)

        expect do
          request
        end.not_to exceed_all_query_limit(control)
      end
    end

    context 'rate limiting' do
      let_it_be(:current_user) { create(:user) }

      shared_examples_for 'does not log request and does not block the request' do
        specify do
          request
          request

          expect(response).not_to have_gitlab_http_status(:too_many_requests)
          expect(Gitlab::AuthLogger).not_to receive(:error)
        end
      end

      before do
        stub_application_setting(projects_api_rate_limit_unauthenticated: 1)
      end

      context 'when the user is signed in' do
        it_behaves_like 'does not log request and does not block the request' do
          def request
            get api(path, current_user)
          end
        end
      end

      context 'when the user is not signed in' do
        let_it_be(:current_user) { nil }

        it_behaves_like 'rate limited endpoint', rate_limit_key: :projects_api_rate_limit_unauthenticated do
          def request
            get api(path, current_user)
          end
        end
      end
    end
  end

  describe 'POST /projects' do
    let(:path) { '/projects' }

    context 'maximum number of projects reached' do
      it 'does not create new project and respond with 403' do
        allow_any_instance_of(User).to receive(:projects_limit_left).and_return(0)
        expect { post api(path, user2), params: { name: 'foo' } }
          .to change { Project.count }.by(0)
        expect(response).to have_gitlab_http_status(:forbidden)
      end
    end

    it 'creates new project without path but with name and returns 201' do
      expect { post api(path, user), params: { name: 'Foo Project' } }
        .to change { Project.count }.by(1)
      expect(response).to have_gitlab_http_status(:created)

      project = Project.last

      expect(project.name).to eq('Foo Project')
      expect(project.path).to eq('foo-project')
    end

    it 'creates new project without name but with path and returns 201' do
      expect { post api(path, user), params: { path: 'foo_project' } }
        .to change { Project.count }.by(1)
      expect(response).to have_gitlab_http_status(:created)

      project = Project.last

      expect(project.name).to eq('foo_project')
      expect(project.path).to eq('foo_project')
    end

    it 'creates new project with name and path and returns 201' do
      expect { post api(path, user), params: { path: 'path-project-Foo', name: 'Foo Project' } }
        .to change { Project.count }.by(1)
      expect(response).to have_gitlab_http_status(:created)

      project = Project.last

      expect(project.name).to eq('Foo Project')
      expect(project.path).to eq('path-project-Foo')
    end

    it_behaves_like 'create project with default branch parameter' do
      subject(:request) { post api(path, user), params: params }
    end

    it 'creates last project before reaching project limit' do
      allow_any_instance_of(User).to receive(:projects_limit_left).and_return(1)
      post api(path, user2), params: { name: 'foo' }
      expect(response).to have_gitlab_http_status(:created)
    end

    it 'does not create new project without name or path and returns 400' do
      expect { post api(path, user) }.not_to change { Project.count }
      expect(response).to have_gitlab_http_status(:bad_request)
    end

    it 'assigns attributes to project' do
      project = attributes_for(:project, {
        path: 'camelCasePath',
        issues_enabled: false,
        jobs_enabled: false,
        merge_requests_enabled: false,
        forking_access_level: 'disabled',
        analytics_access_level: 'disabled',
        wiki_enabled: false,
        resolve_outdated_diff_discussions: false,
        remove_source_branch_after_merge: true,
        autoclose_referenced_issues: true,
        only_allow_merge_if_pipeline_succeeds: true,
        allow_merge_on_skipped_pipeline: true,
        request_access_enabled: true,
        only_allow_merge_if_all_discussions_are_resolved: false,
        ci_config_path: 'a/custom/path',
        merge_method: 'ff',
        squash_option: 'always'
      }).tap do |attrs|
        attrs[:analytics_access_level] = 'disabled'
        attrs[:container_registry_access_level] = 'private'
        attrs[:security_and_compliance_access_level] = 'private'
        attrs[:releases_access_level] = 'disabled'
        attrs[:environments_access_level] = 'disabled'
        attrs[:feature_flags_access_level] = 'disabled'
        attrs[:infrastructure_access_level] = 'disabled'
        attrs[:monitor_access_level] = 'disabled'
        attrs[:snippets_access_level] = 'disabled'
        attrs[:wiki_access_level] = 'disabled'
        attrs[:builds_access_level] = 'disabled'
        attrs[:merge_requests_access_level] = 'disabled'
        attrs[:issues_access_level] = 'disabled'
      end

      post api(path, user), params: project

      expect(response).to have_gitlab_http_status(:created)

      project.each_pair do |k, v|
        next if %i[
          has_external_issue_tracker has_external_wiki issues_enabled merge_requests_enabled wiki_enabled storage_version
          container_registry_access_level releases_access_level environments_access_level feature_flags_access_level
          infrastructure_access_level monitor_access_level
        ].include?(k)

        expect(json_response[k.to_s]).to eq(v)
      end

      # Check feature permissions attributes
      project = Project.find_by_path(project[:path])
      expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED)
      expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::DISABLED)
      expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::DISABLED)
      expect(project.project_feature.analytics_access_level).to eq(ProjectFeature::DISABLED)
      expect(project.project_feature.container_registry_access_level).to eq(ProjectFeature::PRIVATE)
      expect(project.project_feature.security_and_compliance_access_level).to eq(ProjectFeature::PRIVATE)
      expect(project.project_feature.releases_access_level).to eq(ProjectFeature::DISABLED)
      expect(project.project_feature.environments_access_level).to eq(ProjectFeature::DISABLED)
      expect(project.project_feature.feature_flags_access_level).to eq(ProjectFeature::DISABLED)
      expect(project.project_feature.infrastructure_access_level).to eq(ProjectFeature::DISABLED)
      expect(project.project_feature.monitor_access_level).to eq(ProjectFeature::DISABLED)
      expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::DISABLED)
      expect(project.project_feature.builds_access_level).to eq(ProjectFeature::DISABLED)
      expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::DISABLED)
      expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED)
      expect(project.project_feature.snippets_access_level).to eq(ProjectFeature::DISABLED)
    end

    it 'assigns container_registry_enabled to project' do
      project = attributes_for(:project, { container_registry_enabled: true })

      post api(path, user), params: project

      expect(response).to have_gitlab_http_status(:created)
      expect(json_response['container_registry_enabled']).to eq(true)
      expect(json_response['container_registry_access_level']).to eq('enabled')
      expect(Project.find_by(path: project[:path]).container_registry_access_level).to eq(ProjectFeature::ENABLED)
    end

    it 'assigns container_registry_enabled to project' do
      project = attributes_for(:project, { container_registry_enabled: true })

      post api(path, user), params: project

      expect(response).to have_gitlab_http_status(:created)
      expect(json_response['container_registry_enabled']).to eq(true)
      expect(Project.find_by(path: project[:path]).container_registry_access_level).to eq(ProjectFeature::ENABLED)
    end

    it 'creates a project using a template' do
      expect { post api(path, user), params: { template_name: 'rails', name: 'rails-test' } }
        .to change { Project.count }.by(1)

      expect(response).to have_gitlab_http_status(:created)

      project = Project.find(json_response['id'])
      expect(project).to be_saved
      expect(project.import_type).to eq('gitlab_project')
    end

    it 'returns 400 for an invalid template' do
      expect { post api(path, user), params: { template_name: 'unknown', name: 'rails-test' } }
        .not_to change { Project.count }

      expect(response).to have_gitlab_http_status(:bad_request)
      expect(json_response['message']['template_name']).to eq(["'unknown' is unknown or invalid"])
    end

    it 'disallows creating a project with an import_url and template' do
      project_params = { import_url: 'http://example.com', template_name: 'rails', name: 'rails-test' }
      expect { post api(path, user), params: project_params }
        .not_to change {  Project.count }

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

    it 'disallows creating a project with an import_url when git import source is disabled' do
      url = 'http://example.com'
      stub_application_setting(import_sources: nil)

      endpoint_url = "#{url}/info/refs?service=git-upload-pack"
      stub_full_request(endpoint_url, method: :get).to_return(
        { status: 200,
          body: '001e# service=git-upload-pack',
          headers: { 'Content-Type': 'application/x-git-upload-pack-advertisement' } })

      project_params = { import_url: url, path: 'path-project-Foo', name: 'Foo Project' }
      expect { post api(path, user), params: project_params }
        .not_to change {  Project.count }

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

    it 'allows creating a project without an import_url when git import source is disabled' do
      stub_application_setting(import_sources: nil)
      project_params = { path: 'path-project-Foo' }

      expect { post api(path, user), params: project_params }.to change { Project.count }.by(1)

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

    it 'disallows creating a project with an import_url that is not reachable' do
      url = 'http://example.com'
      endpoint_url = "#{url}/info/refs?service=git-upload-pack"
      stub_full_request(endpoint_url, method: :get).to_return({ status: 301, body: '', headers: nil })
      project_params = { import_url: url, path: 'path-project-Foo', name: 'Foo Project' }

      expect { post api(path, user), params: project_params }.not_to change { Project.count }

      expect(response).to have_gitlab_http_status(:unprocessable_entity)
      expect(json_response['message']).to eq("#{url} is not a valid HTTP Git repository")
    end

    it 'creates a project with an import_url that is valid' do
      url = 'http://example.com'
      endpoint_url = "#{url}/info/refs?service=git-upload-pack"
      git_response = {
        status: 200,
        body: '001e# service=git-upload-pack',
        headers: { 'Content-Type': 'application/x-git-upload-pack-advertisement' }
      }
      stub_application_setting(import_sources: ['git'])
      stub_full_request(endpoint_url, method: :get).to_return(git_response)
      project_params = { import_url: url, path: 'path-project-Foo', name: 'Foo Project' }

      expect { post api(path, user), params: project_params }.to change { Project.count }.by(1)

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

    it 'sets a project as public' do
      project = attributes_for(:project, visibility: 'public')

      post api(path, user), params: project

      expect(json_response['visibility']).to eq('public')
    end

    it 'sets a project as internal' do
      project = attributes_for(:project, visibility: 'internal')

      post api(path, user), params: project

      expect(json_response['visibility']).to eq('internal')
    end

    it 'sets a project as private' do
      project = attributes_for(:project, visibility: 'private')

      post api(path, user), params: project

      expect(json_response['visibility']).to eq('private')
    end

    it 'creates a new project initialized with a README.md' do
      project = attributes_for(:project, initialize_with_readme: 1)

      post api(path, user), params: project

      expect(json_response['readme_url']).to eql("#{Gitlab.config.gitlab.url}/#{json_response['namespace']['full_path']}/#{json_response['path']}/-/blob/master/README.md")
    end

    it 'sets tag list to a project (deprecated)' do
      project = attributes_for(:project, tag_list: %w[tagFirst tagSecond])

      post api(path, user), params: project

      expect(json_response['topics']).to eq(%w[tagFirst tagSecond])
    end

    it 'sets topics to a project' do
      project = attributes_for(:project, topics: %w[topic1 topics2])

      post api(path, user), params: project

      expect(json_response['topics']).to eq(%w[topic1 topics2])
    end

    it 'uploads avatar for project a project' do
      project = attributes_for(:project, avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif'))

      workhorse_form_with_file(
        api(path, user),
        method: :post,
        file_key: :avatar,
        params: project
      )

      project_id = json_response['id']
      expect(json_response['avatar_url']).to eq("http://localhost/uploads/-/system/project/avatar/#{project_id}/banana_sample.gif")
    end

    it 'sets a project as not allowing outdated diff discussions to automatically resolve' do
      project = attributes_for(:project, resolve_outdated_diff_discussions: false)

      post api(path, user), params: project

      expect(json_response['resolve_outdated_diff_discussions']).to be_falsey
    end

    it 'sets a project as allowing outdated diff discussions to automatically resolve' do
      project = attributes_for(:project, resolve_outdated_diff_discussions: true)

      post api(path, user), params: project

      expect(json_response['resolve_outdated_diff_discussions']).to be_truthy
    end

    it 'sets a project as not removing source branches' do
      project = attributes_for(:project, remove_source_branch_after_merge: false)

      post api(path, user), params: project

      expect(json_response['remove_source_branch_after_merge']).to be_falsey
    end

    it 'sets a project as removing source branches' do
      project = attributes_for(:project, remove_source_branch_after_merge: true)

      post api(path, user), params: project

      expect(json_response['remove_source_branch_after_merge']).to be_truthy
    end

    it 'sets a project as allowing merge even if build fails' do
      project = attributes_for(:project, only_allow_merge_if_pipeline_succeeds: false)

      post api(path, user), params: project

      expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_falsey
    end

    it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do
      project = attributes_for(:project, only_allow_merge_if_pipeline_succeeds: true)

      post api(path, user), params: project

      expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_truthy
    end

    it 'sets a project as not allowing merge when pipeline is skipped' do
      project_params = attributes_for(:project, allow_merge_on_skipped_pipeline: false)

      post api(path, user), params: project_params

      expect(json_response['allow_merge_on_skipped_pipeline']).to be_falsey
    end

    it 'sets a project as allowing merge when pipeline is skipped' do
      project_params = attributes_for(:project, allow_merge_on_skipped_pipeline: true)

      post api(path, user), params: project_params

      expect(json_response['allow_merge_on_skipped_pipeline']).to be_truthy
    end

    it 'sets a project as allowing merge even if discussions are unresolved' do
      project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: false)

      post api(path, user), params: project

      expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey
    end

    it 'sets a project as allowing merge if only_allow_merge_if_all_discussions_are_resolved is nil' do
      project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: nil)

      post api(path, user), params: project

      expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey
    end

    it 'sets a project as allowing merge only if all discussions are resolved' do
      project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: true)

      post api(path, user), params: project

      expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy
    end

    it 'sets a project as enabling auto close referenced issues' do
      project = attributes_for(:project, autoclose_referenced_issues: true)

      post api(path, user), params: project

      expect(json_response['autoclose_referenced_issues']).to be_truthy
    end

    it 'sets a project as disabling auto close referenced issues' do
      project = attributes_for(:project, autoclose_referenced_issues: false)

      post api(path, user), params: project

      expect(json_response['autoclose_referenced_issues']).to be_falsey
    end

    it 'sets the merge method of a project to rebase merge' do
      project = attributes_for(:project, merge_method: 'rebase_merge')

      post api(path, user), params: project

      expect(json_response['merge_method']).to eq('rebase_merge')
    end

    it 'rejects invalid values for merge_method' do
      project = attributes_for(:project, merge_method: 'totally_not_valid_method')

      post api(path, user), params: project

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

    it 'ignores import_url when it is nil' do
      project = attributes_for(:project, import_url: nil)

      post api(path, user), params: project

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

    context 'when a visibility level is restricted' do
      let(:project_param) { attributes_for(:project, visibility: 'public') }

      before do
        stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
      end

      it 'does not allow a non-admin to use a restricted visibility level' do
        post api(path, user), params: project_param

        expect(response).to have_gitlab_http_status(:bad_request)
        expect(json_response['message']['visibility_level'].first).to(
          match('restricted by your GitLab administrator')
        )
      end

      it 'allows an admin to override restricted visibility settings' do
        post api(path, admin), params: project_param

        expect(json_response['visibility']).to eq('public')
      end
    end
  end

  describe 'GET /users/:user_id/projects/' do
    let_it_be(:public_project) { create(:project, :public, creator_id: user4.id, namespace: user4.namespace) }

    it 'returns error when user not found' do
      get api("/users/#{non_existing_record_id}/projects/")

      expect(response).to have_gitlab_http_status(:not_found)
      expect(json_response['message']).to eq('404 User Not Found')
    end

    it 'returns projects filtered by user id' do
      get api("/users/#{user4.id}/projects/", user)

      expect(response).to have_gitlab_http_status(:ok)
      expect(response).to include_pagination_headers
      expect(json_response).to be_an Array
      expect(json_response.map { |project| project['id'] }).to contain_exactly(public_project.id)
    end

    it 'includes container_registry_access_level' do
      get api("/users/#{user4.id}/projects/", user)

      expect(response).to have_gitlab_http_status(:ok)
      expect(json_response).to be_an Array
      expect(json_response.first.keys).to include('container_registry_access_level')
    end

    context 'filter by updated_at' do
      it 'returns only projects updated on the given timeframe' do
        get api("/users/#{user.id}/projects", user),
          params: { updated_before: 2.days.ago.iso8601, updated_after: 6.days.ago }

        expect(response).to have_gitlab_http_status(:ok)
        expect(json_response.map { |project| project['id'] }).to contain_exactly(project2.id, project.id)
      end
    end

    context 'and using id_after' do
      let_it_be(:another_public_project) { create(:project, :public, creator_id: user4.id, namespace: user4.namespace) }

      it 'only returns projects with id_after filter given' do
        get api("/users/#{user4.id}/projects?id_after=#{public_project.id}", user)

        expect(response).to have_gitlab_http_status(:ok)
        expect(response).to include_pagination_headers
        expect(json_response).to be_an Array
        expect(json_response.map { |project| project['id'] }).to contain_exactly(another_public_project.id)
      end

      it 'returns both projects without a id_after filter' do
        get api("/users/#{user4.id}/projects", user)

        expect(response).to have_gitlab_http_status(:ok)
        expect(response).to include_pagination_headers
        expect(json_response).to be_an Array
        expect(json_response.map { |project| project['id'] }).to contain_exactly(public_project.id, another_public_project.id)
      end
    end

    context 'and using id_before' do
      let_it_be(:another_public_project) { create(:project, :public, creator_id: user4.id, namespace: user4.namespace) }

      it 'only returns projects with id_before filter given' do
        get api("/users/#{user4.id}/projects?id_before=#{another_public_project.id}", user)

        expect(response).to have_gitlab_http_status(:ok)
        expect(response).to include_pagination_headers
        expect(json_response).to be_an Array
        expect(json_response.map { |project| project['id'] }).to contain_exactly(public_project.id)
      end

      it 'returns both projects without a id_before filter' do
        get api("/users/#{user4.id}/projects", user)

        expect(response).to have_gitlab_http_status(:ok)
        expect(response).to include_pagination_headers
        expect(json_response).to be_an Array
        expect(json_response.map { |project| project['id'] }).to contain_exactly(public_project.id, another_public_project.id)
      end
    end

    context 'and using both id_before and id_after' do
      let_it_be(:more_projects) { create_list(:project, 5, :public, creator_id: user4.id, namespace: user4.namespace) }

      it 'only returns projects with id matching the range' do
        get api("/users/#{user4.id}/projects?id_after=#{more_projects.first.id}&id_before=#{more_projects.last.id}", user)

        expect(response).to have_gitlab_http_status(:ok)
        expect(response).to include_pagination_headers
        expect(json_response).to be_an Array
        expect(json_response.map { |project| project['id'] }).to contain_exactly(*more_projects[1..-2].map(&:id))
      end
    end

    it 'returns projects filtered by username' do
      get api("/users/#{user4.username}/projects/", user)

      expect(response).to have_gitlab_http_status(:ok)
      expect(response).to include_pagination_headers
      expect(json_response).to be_an Array
      expect(json_response.map { |project| project['id'] }).to contain_exactly(public_project.id)
    end

    it 'returns projects filtered by minimal access level' do
      private_project1 = create(:project, :private, name: 'private_project1', creator_id: user4.id, namespace: user4.namespace)
      private_project2 = create(:project, :private, name: 'private_project2', creator_id: user4.id, namespace: user4.namespace)
      private_project1.add_developer(user2)
      private_project2.add_reporter(user2)

      get api("/users/#{user4.id}/projects/", user2), params: { min_access_level: 30 }

      expect(response).to have_gitlab_http_status(:ok)
      expect(response).to include_pagination_headers
      expect(json_response).to be_an Array
      expect(json_response.map { |project| project['id'] }).to contain_exactly(private_project1.id)
    end

    context 'and using an admin to search', :enable_admin_mode do
      it 'returns users projects when authenticated as admin' do
        private_project1 = create(:project, :private, name: 'private_project1', creator_id: user4.id, namespace: user4.namespace)

        # min_access_level does not make any difference when admins search for a user's projects
        get api("/users/#{user4.id}/projects/", admin), params: { min_access_level: 30 }

        expect(response).to have_gitlab_http_status(:ok)
        expect(response).to include_pagination_headers
        expect(json_response).to be_an Array
        expect(json_response.map { |project| project['id'] }).to contain_exactly(project4.id, private_project1.id, public_project.id)
      end
    end

    context 'and using the programming language filter' do
      include_context 'with language detection'

      it 'filters case-insensitively by programming language' do
        get api('/projects', user), params: { with_programming_language: 'ruby' }

        expect(response).to have_gitlab_http_status(:ok)
        expect(response).to include_pagination_headers
        expect(json_response).to be_an Array
        expect(json_response.map { |p| p['id'] }).to contain_exactly(project.id)
      end
    end
  end

  describe 'GET /users/:user_id/starred_projects/' do
    before do
      user3.update!(starred_projects: [project, project2, project3])
      user3.reload
    end

    let(:path) { "/users/#{user3.id}/starred_projects/" }

    it 'returns error when user not found' do
      get api("/users/#{non_existing_record_id}/starred_projects/")

      expect(response).to have_gitlab_http_status(:not_found)
      expect(json_response['message']).to eq('404 User Not Found')
    end

    context 'with a public profile' do
      it 'returns projects filtered by user' do
        get api(path, user)

        expect(response).to have_gitlab_http_status(:ok)
        expect(response).to include_pagination_headers
        expect(json_response).to be_an Array
        expect(json_response.map { |project| project['id'] })
          .to contain_exactly(project.id, project2.id, project3.id)
      end

      context 'filter by updated_at' do
        it 'returns only projects updated on the given timeframe' do
          get api(path, user),
            params: { updated_before: 2.days.ago.iso8601, updated_after: 6.days.ago }

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response.map { |project| project['id'] }).to contain_exactly(project2.id, project.id)
        end
      end
    end

    context 'with a private profile' do
      before do
        user3.update!(private_profile: true)
        user3.reload
      end

      context 'user does not have access to view the private profile' do
        it 'returns no projects' do
          get api(path, user)

          expect(response).to have_gitlab_http_status(:ok)
          expect(response).to include_pagination_headers
          expect(json_response).to be_an Array
          expect(json_response).to be_empty
        end
      end

      context 'user has access to view the private profile' do
        it 'returns projects filtered by user' do
          get api(path, admin, admin_mode: true)

          expect(response).to have_gitlab_http_status(:ok)
          expect(response).to include_pagination_headers
          expect(json_response).to be_an Array
          expect(json_response.map { |project| project['id'] })
            .to contain_exactly(project.id, project2.id, project3.id)
        end
      end
    end
  end

  describe 'POST /projects/user/:id' do
    let(:path) { "/projects/user/#{user.id}" }

    it_behaves_like 'POST request permissions for admin mode' do
      let(:params) { { name: 'Foo Project' } }
    end

    it 'creates new project without path but with name and return 201' do
      expect { post api(path, admin, admin_mode: true), params: { name: 'Foo Project' } }.to change { Project.count }.by(1)
      expect(response).to have_gitlab_http_status(:created)

      project = Project.find(json_response['id'])

      expect(project.name).to eq('Foo Project')
      expect(project.path).to eq('foo-project')
    end

    it 'creates new project with name and path and returns 201' do
      expect { post api(path, admin, admin_mode: true), params: { path: 'path-project-Foo', name: 'Foo Project' } }
        .to change { Project.count }.by(1)
      expect(response).to have_gitlab_http_status(:created)

      project = Project.find(json_response['id'])

      expect(project.name).to eq('Foo Project')
      expect(project.path).to eq('path-project-Foo')
    end

    it_behaves_like 'create project with default branch parameter' do
      subject(:request) { post api(path, admin, admin_mode: true), params: params }
    end

    it 'responds with 400 on failure and not project' do
      expect { post api(path, admin, admin_mode: true) }
        .not_to change { Project.count }

      expect(response).to have_gitlab_http_status(:bad_request)
      expect(json_response['error']).to eq('name is missing')
    end

    it 'sets container_registry_enabled' do
      project = attributes_for(:project).tap do |attrs|
        attrs[:container_registry_enabled] = true
      end

      post api(path, admin, admin_mode: true), params: project

      expect(response).to have_gitlab_http_status(:created)
      expect(json_response['container_registry_enabled']).to eq(true)
      expect(Project.find_by(path: project[:path]).container_registry_access_level).to eq(ProjectFeature::ENABLED)
    end

    it 'assigns attributes to project' do
      project = attributes_for(:project, {
        issues_enabled: false,
        merge_requests_enabled: false,
        wiki_enabled: false,
        request_access_enabled: true,
        jobs_enabled: true
      })

      post api(path, admin, admin_mode: true), params: project

      expect(response).to have_gitlab_http_status(:created)

      project.each_pair do |k, v|
        next if %i[has_external_issue_tracker has_external_wiki path storage_version].include?(k)

        expect(json_response[k.to_s]).to eq(v)
      end
    end

    it 'sets a project as public' do
      project = attributes_for(:project, visibility: 'public')

      post api(path, admin, admin_mode: true), params: project

      expect(response).to have_gitlab_http_status(:created)
      expect(json_response['visibility']).to eq('public')
    end

    it 'sets a project as internal' do
      project = attributes_for(:project, visibility: 'internal')

      post api(path, admin, admin_mode: true), params: project

      expect(response).to have_gitlab_http_status(:created)
      expect(json_response['visibility']).to eq('internal')
    end

    it 'sets a project as private' do
      project = attributes_for(:project, visibility: 'private')

      post api(path, admin, admin_mode: true), params: project

      expect(json_response['visibility']).to eq('private')
    end

    it 'sets a project as not allowing outdated diff discussions to automatically resolve' do
      project = attributes_for(:project, resolve_outdated_diff_discussions: false)

      post api(path, admin, admin_mode: true), params: project

      expect(json_response['resolve_outdated_diff_discussions']).to be_falsey
    end

    it 'sets a project as allowing outdated diff discussions to automatically resolve' do
      project = attributes_for(:project, resolve_outdated_diff_discussions: true)

      post api(path, admin, admin_mode: true), params: project

      expect(json_response['resolve_outdated_diff_discussions']).to be_truthy
    end

    it 'sets a project as not removing source branches' do
      project = attributes_for(:project, remove_source_branch_after_merge: false)

      post api(path, admin, admin_mode: true), params: project

      expect(json_response['remove_source_branch_after_merge']).to be_falsey
    end

    it 'sets a project as removing source branches' do
      project = attributes_for(:project, remove_source_branch_after_merge: true)

      post api(path, admin, admin_mode: true), params: project

      expect(json_response['remove_source_branch_after_merge']).to be_truthy
    end

    it 'sets a project as allowing merge even if build fails' do
      project = attributes_for(:project, only_allow_merge_if_pipeline_succeeds: false)

      post api(path, admin, admin_mode: true), params: project

      expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_falsey
    end

    it 'sets a project as allowing merge only if pipeline succeeds' do
      project = attributes_for(:project, only_allow_merge_if_pipeline_succeeds: true)

      post api(path, admin, admin_mode: true), params: project

      expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_truthy
    end

    it 'sets a project as not allowing merge when pipeline is skipped' do
      project = attributes_for(:project, allow_merge_on_skipped_pipeline: false)

      post api(path, admin, admin_mode: true), params: project

      expect(json_response['allow_merge_on_skipped_pipeline']).to be_falsey
    end

    it 'sets a project as allowing merge when pipeline is skipped' do
      project = attributes_for(:project, allow_merge_on_skipped_pipeline: true)

      post api(path, admin, admin_mode: true), params: project

      expect(json_response['allow_merge_on_skipped_pipeline']).to be_truthy
    end

    it 'sets a project as allowing merge even if discussions are unresolved' do
      project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: false)

      post api(path, admin, admin_mode: true), params: project

      expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey
    end

    it 'sets a project as allowing merge only if all discussions are resolved' do
      project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: true)

      post api(path, admin, admin_mode: true), params: project

      expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy
    end

    context 'container_registry_enabled' do
      using RSpec::Parameterized::TableSyntax

      where(:container_registry_enabled, :container_registry_access_level) do
        true  | ProjectFeature::ENABLED
        false | ProjectFeature::DISABLED
      end

      with_them do
        it 'setting container_registry_enabled also sets container_registry_access_level' do
          project_attributes = attributes_for(:project).tap do |attrs|
            attrs[:container_registry_enabled] = container_registry_enabled
          end

          post api(path, admin, admin_mode: true), params: project_attributes

          project = Project.find_by(path: project_attributes[:path])
          expect(response).to have_gitlab_http_status(:created)
          expect(json_response['container_registry_access_level']).to eq(ProjectFeature.str_from_access_level(container_registry_access_level))
          expect(json_response['container_registry_enabled']).to eq(container_registry_enabled)
          expect(project.container_registry_access_level).to eq(container_registry_access_level)
          expect(project.container_registry_enabled).to eq(container_registry_enabled)
        end
      end
    end

    context 'container_registry_access_level' do
      using RSpec::Parameterized::TableSyntax

      where(:container_registry_access_level, :container_registry_enabled) do
        'enabled'  | true
        'private'  | true
        'disabled' | false
      end

      with_them do
        it 'setting container_registry_access_level also sets container_registry_enabled' do
          project_attributes = attributes_for(:project).tap do |attrs|
            attrs[:container_registry_access_level] = container_registry_access_level
          end

          post api(path, admin, admin_mode: true), params: project_attributes

          project = Project.find_by(path: project_attributes[:path])
          expect(response).to have_gitlab_http_status(:created)
          expect(json_response['container_registry_access_level']).to eq(container_registry_access_level)
          expect(json_response['container_registry_enabled']).to eq(container_registry_enabled)
          expect(project.container_registry_access_level).to eq(ProjectFeature.access_level_from_str(container_registry_access_level))
          expect(project.container_registry_enabled).to eq(container_registry_enabled)
        end
      end
    end
  end

  describe "POST /projects/:id/uploads/authorize" do
    let(:headers) { workhorse_internal_api_request_header.merge({ 'HTTP_GITLAB_WORKHORSE' => 1 }) }
    let(:path) { "/projects/#{project.id}/uploads/authorize" }

    context 'with authorized user' do
      it "returns 200" do
        post api(path, user), headers: headers

        expect(response).to have_gitlab_http_status(:ok)
        expect(json_response['MaximumSize']).to eq(project.max_attachment_size)
      end
    end

    context 'with unauthorized user' do
      it "returns 404" do
        post api(path, user2), headers: headers

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

    context 'with exempted project' do
      before do
        stub_env('GITLAB_UPLOAD_API_ALLOWLIST', project.id)
      end

      it "returns 200" do
        post api(path, user), headers: headers

        expect(response).to have_gitlab_http_status(:ok)
        expect(json_response['MaximumSize']).to eq(1.gigabyte)
      end
    end

    context 'with no Workhorse headers' do
      it "returns 403" do
        post api(path, user)

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

  describe "POST /projects/:id/uploads" do
    let(:file) { fixture_file_upload("spec/fixtures/dk.png", "image/png") }
    let(:path) { "/projects/#{project.id}/uploads" }

    before do
      project
    end

    it "uploads the file and returns its info" do
      expect_next_instance_of(UploadService) do |instance|
        expect(instance).to receive(:override_max_attachment_size=).with(project.max_attachment_size).and_call_original
      end

      post api(path, user), params: { file: file }

      expect(response).to have_gitlab_http_status(:created)
      expect(json_response['alt']).to eq("dk")
      expect(json_response['url']).to start_with("/uploads/")
      expect(json_response['url']).to end_with("/dk.png")
      expect(json_response['full_path']).to start_with("/#{project.namespace.path}/#{project.path}/uploads")
    end

    it "does not leave the temporary file in place after uploading, even when the tempfile reaper does not run" do
      tempfile = Tempfile.new('foo')
      path = tempfile.path

      allow_any_instance_of(Rack::TempfileReaper).to receive(:call) do |instance, env|
        instance.instance_variable_get(:@app).call(env)
      end

      expect(path).not_to be(nil)
      expect(Rack::Multipart::Parser::TEMPFILE_FACTORY).to receive(:call).and_return(tempfile)

      post api(path, user), params: { file: fixture_file_upload("spec/fixtures/dk.png", "image/png") }

      expect(tempfile.path).to be(nil)
      expect(File.exist?(path)).to be(false)
    end

    shared_examples 'capped upload attachments' do |upload_allowed|
      it "limits the upload to 1 GB" do
        expect_next_instance_of(UploadService) do |instance|
          expect(instance).to receive(:override_max_attachment_size=).with(1.gigabyte).and_call_original
        end

        post api(path, user), params: { file: file }

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

      it "logs a warning if file exceeds attachment size" do
        allow(Gitlab::CurrentSettings).to receive(:max_attachment_size).and_return(0)

        expect(Gitlab::AppLogger).to receive(:info).with(
          hash_including(message: 'File exceeds maximum size', upload_allowed: upload_allowed))
            .and_call_original

        post api(path, user), params: { file: file }
      end
    end

    context 'with exempted project' do
      before do
        stub_env('GITLAB_UPLOAD_API_ALLOWLIST', project.id)
      end

      it_behaves_like 'capped upload attachments', true
    end
  end

  describe "GET /projects/:id/groups" do
    let_it_be(:root_group) { create(:group, :public, name: 'root group') }
    let_it_be(:project_group) { create(:group, :public, parent: root_group, name: 'project group') }
    let_it_be(:shared_group_with_dev_access) { create(:group, :private, parent: root_group, name: 'shared group') }
    let_it_be(:shared_group_with_reporter_access) { create(:group, :public) }
    let_it_be(:private_project) { create(:project, :private, group: project_group) }
    let_it_be(:public_project) { create(:project, :public, group: project_group) }

    let(:path) { "/projects/#{private_project.id}/groups" }

    before_all do
      create(:project_group_link, :developer, group: shared_group_with_dev_access, project: private_project)
      create(:project_group_link, :reporter, group: shared_group_with_reporter_access, project: private_project)
    end

    it_behaves_like 'GET request permissions for admin mode' do
      let(:failed_status_code) { :not_found }
    end

    shared_examples_for 'successful groups response' do
      it 'returns an array of groups' do
        request

        expect(response).to have_gitlab_http_status(:ok)
        expect(response).to include_pagination_headers
        expect(json_response).to be_an Array
        expect(json_response.map { |g| g['name'] }).to match_array(expected_groups.map(&:name))
      end
    end

    context 'when unauthenticated' do
      it 'does not return groups for private projects' do
        get api(path)

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

      context 'for public projects' do
        subject(:request) { get api("/projects/#{public_project.id}/groups") }

        it_behaves_like 'successful groups response' do
          let(:expected_groups) { [root_group, project_group] }
        end
      end
    end

    context 'when authenticated as user' do
      context 'when user does not have access to the project' do
        it 'does not return groups' do
          get api(path, user)

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

      context 'when user has access to the project' do
        subject(:request) { get api(path, user), params: params }

        let(:params) { {} }

        before do
          private_project.add_developer(user)
        end

        it_behaves_like 'successful groups response' do
          let(:expected_groups) { [root_group, project_group] }
        end

        context 'when search by root group name' do
          let(:params) { { search: 'root' } }

          it_behaves_like 'successful groups response' do
            let(:expected_groups) { [root_group] }
          end
        end

        context 'with_shared option is on' do
          let(:params) { { with_shared: true } }

          it_behaves_like 'successful groups response' do
            let(:expected_groups) { [root_group, project_group, shared_group_with_dev_access, shared_group_with_reporter_access] }
          end

          context 'when shared_min_access_level is set' do
            let(:params) { super().merge(shared_min_access_level: Gitlab::Access::DEVELOPER) }

            it_behaves_like 'successful groups response' do
              let(:expected_groups) { [root_group, project_group, shared_group_with_dev_access] }
            end
          end

          context 'when shared_visible_only is on' do
            let(:params) { super().merge(shared_visible_only: true) }

            it_behaves_like 'successful groups response' do
              let(:expected_groups) { [root_group, project_group, shared_group_with_reporter_access] }
            end
          end

          context 'when search by shared group name' do
            let(:params) { super().merge(search: 'shared') }

            it_behaves_like 'successful groups response' do
              let(:expected_groups) { [shared_group_with_dev_access] }
            end
          end

          context 'when skip_groups is set' do
            let(:params) { super().merge(skip_groups: [shared_group_with_dev_access.id, root_group.id]) }

            it_behaves_like 'successful groups response' do
              let(:expected_groups) { [shared_group_with_reporter_access, project_group] }
            end
          end
        end
      end
    end

    context 'when authenticated as admin' do
      subject(:request) { get api(path, admin, admin_mode: true) }

      it_behaves_like 'successful groups response' do
        let(:expected_groups) { [root_group, project_group] }
      end
    end
  end

  describe 'GET /project/:id/share_locations' do
    let_it_be(:root_group) { create(:group, :public, name: 'root group', path: 'root-group-path') }
    let_it_be(:project_group1) { create(:group, :public, parent: root_group, name: 'group1', path: 'group-1-path') }
    let_it_be(:project_group2) { create(:group, :public, parent: root_group, name: 'group2', path: 'group-2-path') }
    let_it_be(:project) { create(:project, :private, group: project_group1) }
    let(:path) { "/projects/#{project.id}/share_locations" }

    it_behaves_like 'GET request permissions for admin mode' do
      let(:failed_status_code) { :not_found }
    end

    shared_examples_for 'successful groups response' do
      it 'returns an array of groups' do
        request

        expect(response).to have_gitlab_http_status(:ok)
        expect(response).to include_pagination_headers
        expect(json_response).to be_an Array
        expect(json_response.map { |g| g['name'] }).to match_array(expected_groups.map(&:name))
      end
    end

    context 'when unauthenticated' do
      it 'does not return the groups for the given project' do
        get api(path)

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

    context 'when authenticated' do
      context 'when user is not the owner of the project' do
        it 'does not return the groups' do
          get api(path, user)

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

      context 'when user is the owner of the project' do
        subject(:request) { get api(path, user), params: params }

        let(:params) { {} }

        before do
          project.add_owner(user)
          project_group1.add_developer(user)
          project_group2.add_developer(user)
        end

        context 'with default search' do
          it_behaves_like 'successful groups response' do
            let(:expected_groups) { [project_group2] }
          end
        end

        context 'when searching by group name' do
          context 'searching by group name' do
            it_behaves_like 'successful groups response' do
              let(:params) { { search: 'group2' } }
              let(:expected_groups) { [project_group2] }
            end
          end

          context 'searching by full group path' do
            let_it_be(:project_group2_subgroup) do
              create(:group, :public, parent: project_group2, name: 'subgroup', path: 'subgroup-path')
            end

            it_behaves_like 'successful groups response' do
              let(:params) { { search: 'root-group-path/group-2-path/subgroup-path' } }
              let(:expected_groups) { [project_group2_subgroup] }
            end
          end
        end
      end
    end

    context 'when authenticated as admin' do
      subject(:request) { get api(path, admin, admin_mode: true), params: {} }

      context 'without share_with_group_lock' do
        it_behaves_like 'successful groups response' do
          let(:expected_groups) { [project_group2] }
        end
      end

      context 'with share_with_group_lock' do
        before do
          project.namespace.update!(share_with_group_lock: true)
        end

        it_behaves_like 'successful groups response' do
          let(:expected_groups) { [] }
        end
      end
    end
  end

  describe 'GET /projects/:id' do
    let(:path) { "/projects/#{project.id}" }

    it_behaves_like 'GET request permissions for admin mode' do
      let(:failed_status_code) { :not_found }
    end

    context 'when unauthenticated' do
      it 'does not return private projects' do
        private_project = create(:project, :private)

        get api("/projects/#{private_project.id}")

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

      it 'returns public projects' do
        public_project = create(:project, :repository, :public)

        get api("/projects/#{public_project.id}")

        expect(response).to have_gitlab_http_status(:ok)
        expect(json_response['id']).to eq(public_project.id)
        expect(json_response['description']).to eq(public_project.description)
        expect(json_response['default_branch']).to eq(public_project.default_branch)
        expect(json_response['ci_config_path']).to eq(public_project.ci_config_path)
        expect(json_response.keys).not_to include('permissions')
      end

      context 'the project is a public fork' do
        it 'hides details of a public fork parent' do
          public_project = create(:project, :repository, :public)
          fork = fork_project(public_project)

          get api("/projects/#{fork.id}")

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response['forked_from_project']).to be_nil
        end
      end

      context 'and the project has a private repository' do
        let(:project) { create(:project, :repository, :public, :repository_private) }
        let(:protected_attributes) { %w(default_branch ci_config_path) }

        it 'hides protected attributes of private repositories if user is not a member' do
          get api(path, user)

          expect(response).to have_gitlab_http_status(:ok)
          protected_attributes.each do |attribute|
            expect(json_response.keys).not_to include(attribute)
          end
        end

        it 'exposes protected attributes of private repositories if user is a member' do
          project.add_developer(user)

          get api(path, user)

          expect(response).to have_gitlab_http_status(:ok)
          protected_attributes.each do |attribute|
            expect(json_response.keys).to include(attribute)
          end
        end
      end
    end

    context 'when authenticated as an admin', :with_license do
      before do
        stub_container_registry_config(enabled: true, host_port: 'registry.example.org:5000')
      end

      let(:project_attributes_file) { 'spec/requests/api/project_attributes.yml' }
      let(:project_attributes) { YAML.load_file(project_attributes_file) }

      let(:expected_keys) do
        keys = project_attributes.flat_map do |relation, relation_config|
          begin
            actual_keys = project.send(relation).attributes.keys
          rescue NoMethodError
            actual_keys = ["#{relation} is nil"]
          end
          unexposed_attributes = relation_config['unexposed_attributes'] || []
          remapped_attributes = relation_config['remapped_attributes'] || {}
          computed_attributes = relation_config['computed_attributes'] || []
          actual_keys - unexposed_attributes - remapped_attributes.keys + remapped_attributes.values + computed_attributes
        end

        unless Gitlab.ee?
          keys -= %w[
            approvals_before_merge
            compliance_frameworks
            mirror
            requirements_access_level
            requirements_enabled
            security_and_compliance_enabled
            issues_template
            merge_requests_template
          ]
        end

        keys
      end

      it 'returns a project by id' do
        project
        project_member
        group = create(:group)
        link = create(:project_group_link, project: project, group: group)

        get api(path, admin, admin_mode: true)

        expect(response).to have_gitlab_http_status(:ok)
        expect(json_response['id']).to eq(project.id)
        expect(json_response['description']).to eq(project.description)
        expect(json_response['description_html']).to eq(project.description_html)
        expect(json_response['default_branch']).to eq(project.default_branch)
        expect(json_response['tag_list']).to be_an Array # deprecated in favor of 'topics'
        expect(json_response['topics']).to be_an Array
        expect(json_response['archived']).to be_falsey
        expect(json_response['visibility']).to be_present
        expect(json_response['ssh_url_to_repo']).to be_present
        expect(json_response['http_url_to_repo']).to be_present
        expect(json_response['web_url']).to be_present
        expect(json_response['container_registry_image_prefix']).to eq("registry.example.org:5000/#{project.full_path}")
        expect(json_response['owner']).to be_a Hash
        expect(json_response['name']).to eq(project.name)
        expect(json_response['path']).to be_present
        expect(json_response['issues_enabled']).to be_present
        expect(json_response['merge_requests_enabled']).to be_present
        expect(json_response['can_create_merge_request_in']).to be_present
        expect(json_response['wiki_enabled']).to be_present
        expect(json_response['jobs_enabled']).to be_present
        expect(json_response['snippets_enabled']).to be_present
        expect(json_response['container_registry_enabled']).to be_present
        expect(json_response['container_registry_access_level']).to be_present
        expect(json_response['created_at']).to be_present
        expect(json_response['updated_at']).to be_present
        expect(json_response['last_activity_at']).to be_present
        expect(json_response['shared_runners_enabled']).to be_present
        expect(json_response['group_runners_enabled']).to be_present
        expect(json_response['creator_id']).to be_present
        expect(json_response['namespace']).to be_present
        expect(json_response['avatar_url']).to be_nil
        expect(json_response['star_count']).to be_present
        expect(json_response['forks_count']).to be_present
        expect(json_response['public_jobs']).to be_present
        expect(json_response['shared_with_groups']).to be_an Array
        expect(json_response['shared_with_groups'].length).to eq(1)
        expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id)
        expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name)
        expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access)
        expect(json_response['only_allow_merge_if_pipeline_succeeds']).to eq(project.only_allow_merge_if_pipeline_succeeds)
        expect(json_response['allow_merge_on_skipped_pipeline']).to eq(project.allow_merge_on_skipped_pipeline)
        expect(json_response['restrict_user_defined_variables']).to eq(project.restrict_user_defined_variables?)
        expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved)
        expect(json_response['security_and_compliance_access_level']).to be_present
        expect(json_response['releases_access_level']).to be_present
        expect(json_response['environments_access_level']).to be_present
        expect(json_response['feature_flags_access_level']).to be_present
        expect(json_response['infrastructure_access_level']).to be_present
        expect(json_response['monitor_access_level']).to be_present
      end

      it 'exposes all necessary attributes' do
        create(:project_group_link, project: project)

        get api(path, admin, admin_mode: true)

        diff = Set.new(json_response.keys) ^ Set.new(expected_keys)

        expect(diff).to be_empty, failure_message(diff)
      end

      def failure_message(_diff)
        <<~MSG
          It looks like project's set of exposed attributes is different from the expected set.

          The following attributes are missing or newly added:
          {diff.to_a.to_sentence}

          Please update #{project_attributes_file} file"
        MSG
      end
    end

    context 'when authenticated as a regular user' do
      before do
        project
        project_member
        stub_container_registry_config(enabled: true, host_port: 'registry.example.org:5000')
      end

      it 'returns a project by id' do
        group = create(:group)
        link = create(:project_group_link, project: project, group: group)

        get api(path, user)

        expect(response).to have_gitlab_http_status(:ok)
        expect(json_response['id']).to eq(project.id)
        expect(json_response['description']).to eq(project.description)
        expect(json_response['default_branch']).to eq(project.default_branch)
        expect(json_response['tag_list']).to be_an Array # deprecated in favor of 'topics'
        expect(json_response['topics']).to be_an Array
        expect(json_response['archived']).to be_falsey
        expect(json_response['visibility']).to be_present
        expect(json_response['ssh_url_to_repo']).to be_present
        expect(json_response['http_url_to_repo']).to be_present
        expect(json_response['web_url']).to be_present
        expect(json_response['container_registry_image_prefix']).to eq("registry.example.org:5000/#{project.full_path}")
        expect(json_response['owner']).to be_a Hash
        expect(json_response['name']).to eq(project.name)
        expect(json_response['path']).to be_present
        expect(json_response['issues_enabled']).to be_present
        expect(json_response['merge_requests_enabled']).to be_present
        expect(json_response['can_create_merge_request_in']).to be_present
        expect(json_response['wiki_enabled']).to be_present
        expect(json_response['jobs_enabled']).to be_present
        expect(json_response['snippets_enabled']).to be_present
        expect(json_response['snippets_access_level']).to be_present
        expect(json_response['pages_access_level']).to be_present
        expect(json_response['repository_access_level']).to be_present
        expect(json_response['issues_access_level']).to be_present
        expect(json_response['merge_requests_access_level']).to be_present
        expect(json_response['forking_access_level']).to be_present
        expect(json_response['analytics_access_level']).to be_present
        expect(json_response['wiki_access_level']).to be_present
        expect(json_response['builds_access_level']).to be_present
        expect(json_response['security_and_compliance_access_level']).to be_present
        expect(json_response['releases_access_level']).to be_present
        expect(json_response['environments_access_level']).to be_present
        expect(json_response['feature_flags_access_level']).to be_present
        expect(json_response['infrastructure_access_level']).to be_present
        expect(json_response['monitor_access_level']).to be_present
        expect(json_response).to have_key('emails_disabled')
        expect(json_response['resolve_outdated_diff_discussions']).to eq(project.resolve_outdated_diff_discussions)
        expect(json_response['remove_source_branch_after_merge']).to be_truthy
        expect(json_response['container_registry_enabled']).to be_present
        expect(json_response['container_registry_access_level']).to be_present
        expect(json_response['created_at']).to be_present
        expect(json_response['last_activity_at']).to be_present
        expect(json_response['shared_runners_enabled']).to be_present
        expect(json_response['group_runners_enabled']).to be_present
        expect(json_response['creator_id']).to be_present
        expect(json_response['namespace']).to be_present
        expect(json_response['import_status']).to be_present
        expect(json_response).to include("import_error")
        expect(json_response).to have_key('avatar_url')
        expect(json_response['star_count']).to be_present
        expect(json_response['forks_count']).to be_present
        expect(json_response['public_jobs']).to be_present
        expect(json_response).to have_key('ci_config_path')
        expect(json_response['shared_with_groups']).to be_an Array
        expect(json_response['shared_with_groups'].length).to eq(1)
        expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id)
        expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name)
        expect(json_response['shared_with_groups'][0]['group_full_path']).to eq(group.full_path)
        expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access)
        expect(json_response['shared_with_groups'][0]).to have_key('expires_at')
        expect(json_response['only_allow_merge_if_pipeline_succeeds']).to eq(project.only_allow_merge_if_pipeline_succeeds)
        expect(json_response['allow_merge_on_skipped_pipeline']).to eq(project.allow_merge_on_skipped_pipeline)
        expect(json_response['restrict_user_defined_variables']).to eq(project.restrict_user_defined_variables?)
        expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved)
        expect(json_response['ci_default_git_depth']).to eq(project.ci_default_git_depth)
        expect(json_response['ci_forward_deployment_enabled']).to eq(project.ci_forward_deployment_enabled)
        expect(json_response['ci_allow_fork_pipelines_to_run_in_parent_project']).to eq(project.ci_allow_fork_pipelines_to_run_in_parent_project)
        expect(json_response['ci_separated_caches']).to eq(project.ci_separated_caches)
        expect(json_response['merge_method']).to eq(project.merge_method.to_s)
        expect(json_response['squash_option']).to eq(project.squash_option.to_s)
        expect(json_response['readme_url']).to eq(project.readme_url)
        expect(json_response).to have_key 'packages_enabled'
        expect(json_response['keep_latest_artifact']).to be_present
      end

      it 'returns a group link with expiration date' do
        group = create(:group)
        expires_at = 5.days.from_now.to_date
        link = create(:project_group_link, project: project, group: group, expires_at: expires_at)

        get api(path, user)

        expect(json_response['shared_with_groups']).to be_an Array
        expect(json_response['shared_with_groups'].length).to eq(1)
        expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id)
        expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name)
        expect(json_response['shared_with_groups'][0]['group_full_path']).to eq(group.full_path)
        expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access)
        expect(json_response['shared_with_groups'][0]['expires_at']).to eq(expires_at.to_s)
      end

      it 'returns a project by path name' do
        get api(path, user)
        expect(response).to have_gitlab_http_status(:ok)
        expect(json_response['name']).to eq(project.name)
      end

      it 'returns a 404 error if not found' do
        get api("/projects/#{non_existing_record_id}", user)
        expect(response).to have_gitlab_http_status(:not_found)
        expect(json_response['message']).to eq('404 Project Not Found')
      end

      it 'returns a 404 error if user is not a member' do
        other_user = create(:user)
        get api(path, other_user)
        expect(response).to have_gitlab_http_status(:not_found)
      end

      it 'handles users with dots' do
        dot_user = create(:user, username: 'dot.user')
        project = create(:project, creator_id: dot_user.id, namespace: dot_user.namespace)

        get api("/projects/#{CGI.escape(project.full_path)}", dot_user)
        expect(response).to have_gitlab_http_status(:ok)
        expect(json_response['name']).to eq(project.name)
      end

      it 'exposes namespace fields' do
        get api(path, user)

        expect(response).to have_gitlab_http_status(:ok)
        expect(json_response['namespace']).to eq({
          'id' => user.namespace.id,
          'name' => user.namespace.name,
          'path' => user.namespace.path,
          'kind' => user.namespace.kind,
          'full_path' => user.namespace.full_path,
          'parent_id' => nil,
          'avatar_url' => user.avatar_url,
          'web_url' => Gitlab::Routing.url_helpers.user_url(user)
        })
      end

      it "does not include license fields by default" do
        get api(path, user)

        expect(response).to have_gitlab_http_status(:ok)
        expect(json_response).not_to include('license', 'license_url')
      end

      it 'includes license fields when requested' do
        get api(path, user), params: { license: true }

        expect(response).to have_gitlab_http_status(:ok)
        expect(json_response['license']).to eq({
          'key' => project.repository.license.key,
          'name' => project.repository.license.name,
          'nickname' => project.repository.license.nickname,
          'html_url' => project.repository.license.url,
          'source_url' => nil
        })
      end

      it "does not include statistics by default" do
        get api(path, user)

        expect(response).to have_gitlab_http_status(:ok)
        expect(json_response).not_to include 'statistics'
      end

      it "includes statistics if requested" do
        get api(path, user), params: { statistics: true }

        expect(response).to have_gitlab_http_status(:ok)
        expect(json_response).to include 'statistics'
      end

      context "and the project has a private repository" do
        let(:project) { create(:project, :public, :repository, :repository_private) }

        it "does not include statistics if user is not a member" do
          get api(path, user), params: { statistics: true }

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response).not_to include 'statistics'
        end

        it "includes statistics if user is a member" do
          project.add_developer(user)

          get api(path, user), params: { statistics: true }

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response).to include 'statistics'
        end

        it "includes statistics also when repository is disabled" do
          project.add_developer(user)
          project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED)

          get api(path, user), params: { statistics: true }

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response).to include 'statistics'
        end
      end

      it "includes import_error if user can admin project" do
        get api(path, user)

        expect(response).to have_gitlab_http_status(:ok)
        expect(json_response).to include("import_error")
      end

      it "does not include import_error if user cannot admin project" do
        get api(path, user3)

        expect(response).to have_gitlab_http_status(:ok)
        expect(json_response).not_to include("import_error")
      end

      it 'returns 404 when project is marked for deletion' do
        project.update!(pending_delete: true)

        get api(path, user)

        expect(response).to have_gitlab_http_status(:not_found)
        expect(json_response['message']).to eq('404 Project Not Found')
      end

      context 'links exposure' do
        it 'exposes related resources full URIs' do
          get api(path, user)

          links = json_response['_links']

          expect(links['self']).to end_with("/api/v4/projects/#{project.id}")
          expect(links['issues']).to end_with("/api/v4/projects/#{project.id}/issues")
          expect(links['merge_requests']).to end_with("/api/v4/projects/#{project.id}/merge_requests")
          expect(links['repo_branches']).to end_with("/api/v4/projects/#{project.id}/repository/branches")
          expect(links['labels']).to end_with("/api/v4/projects/#{project.id}/labels")
          expect(links['events']).to end_with("/api/v4/projects/#{project.id}/events")
          expect(links['members']).to end_with("/api/v4/projects/#{project.id}/members")
          expect(links['cluster_agents']).to end_with("/api/v4/projects/#{project.id}/cluster_agents")
        end

        it 'filters related URIs when their feature is not enabled' do
          project = create(:project, :public,
                           :merge_requests_disabled,
                           :issues_disabled,
                           creator_id: user.id,
                           namespace: user.namespace)

          get api("/projects/#{project.id}", user)

          links = json_response['_links']

          expect(links.has_key?('merge_requests')).to be_falsy
          expect(links.has_key?('issues')).to be_falsy
          expect(links['self']).to end_with("/api/v4/projects/#{project.id}")
        end
      end

      context 'the project is a fork' do
        it 'shows details of a visible fork parent' do
          fork = fork_project(project, user)

          get api("/projects/#{fork.id}", user)

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response['forked_from_project']).to include('id' => project.id)
        end

        it 'hides details of a hidden fork parent' do
          fork = fork_project(project, user)
          fork_user = create(:user)
          fork.team.add_developer(fork_user)

          get api("/projects/#{fork.id}", fork_user)

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response['forked_from_project']).to be_nil
        end
      end

      describe 'permissions' do
        context 'all projects' do
          before do
            project.add_maintainer(user)
          end

          it 'contains permission information' do
            get api("/projects", user)

            expect(response).to have_gitlab_http_status(:ok)
            detail_of_project = json_response.find { |detail| detail['id'] == project.id }

            expect(detail_of_project.dig('permissions', 'project_access', 'access_level'))
            .to eq(Gitlab::Access::MAINTAINER)
            expect(detail_of_project.dig('permissions', 'group_access')).to be_nil
          end
        end

        context 'personal project' do
          it 'sets project access and returns 200' do
            project.add_maintainer(user)
            get api(path, user)

            expect(response).to have_gitlab_http_status(:ok)
            expect(json_response['permissions']['project_access']['access_level'])
            .to eq(Gitlab::Access::MAINTAINER)
            expect(json_response['permissions']['group_access']).to be_nil
          end
        end

        context 'group project' do
          let(:project2) { create(:project, group: create(:group)) }

          before do
            project2.group.add_owner(user)
          end

          it 'sets the owner and return 200' do
            get api("/projects/#{project2.id}", user)

            expect(response).to have_gitlab_http_status(:ok)
            expect(json_response['permissions']['project_access']).to be_nil
            expect(json_response['permissions']['group_access']['access_level'])
            .to eq(Gitlab::Access::OWNER)
          end
        end

        context 'nested group project' do
          let(:group) { create(:group) }
          let(:nested_group) { create(:group, parent: group) }
          let(:project2) { create(:project, group: nested_group) }

          before do
            project2.group.parent.add_owner(user)
          end

          it 'sets group access and return 200' do
            get api("/projects/#{project2.id}", user)

            expect(response).to have_gitlab_http_status(:ok)
            expect(json_response['permissions']['project_access']).to be_nil
            expect(json_response['permissions']['group_access']['access_level'])
            .to eq(Gitlab::Access::OWNER)
          end

          context 'with various access levels across nested groups' do
            before do
              project2.group.add_maintainer(user)
            end

            it 'sets the maximum group access and return 200' do
              get api("/projects/#{project2.id}", user)

              expect(response).to have_gitlab_http_status(:ok)
              expect(json_response['permissions']['project_access']).to be_nil
              expect(json_response['permissions']['group_access']['access_level'])
              .to eq(Gitlab::Access::OWNER)
            end
          end
        end
      end

      context 'when project belongs to a group namespace' do
        let(:group) { create(:group, :with_avatar) }
        let(:project) { create(:project, namespace: group) }
        let!(:project_member) { create(:project_member, :developer, user: user, project: project) }

        it 'returns group web_url and avatar_url' do
          get api(path, user)

          expect(response).to have_gitlab_http_status(:ok)

          group_data = json_response['namespace']
          expect(group_data['web_url']).to eq(group.web_url)
          expect(group_data['avatar_url']).to eq(group.avatar_url)
        end
      end

      context 'when project belongs to a user namespace' do
        let(:user) { create(:user) }
        let(:project) { create(:project, namespace: user.namespace) }

        it 'returns user web_url and avatar_url' do
          get api(path, user)

          expect(response).to have_gitlab_http_status(:ok)

          user_data = json_response['namespace']
          expect(user_data['web_url']).to eq("http://localhost/#{user.username}")
          expect(user_data['avatar_url']).to eq(user.avatar_url)
        end
      end
    end

    context 'when authenticated as a developer' do
      before do
        project
        project_member
      end

      it 'hides sensitive admin attributes' do
        get api(path, user3)

        expect(response).to have_gitlab_http_status(:ok)
        expect(json_response['id']).to eq(project.id)
        expect(json_response['description']).to eq(project.description)
        expect(json_response['default_branch']).to eq(project.default_branch)
        expect(json_response['ci_config_path']).to eq(project.ci_config_path)
        expect(json_response['forked_from_project']).to eq(project.forked_from_project)
        expect(json_response['service_desk_address']).to eq(project.service_desk_address)
        expect(json_response).not_to include(
          'ci_default_git_depth',
          'ci_forward_deployment_enabled',
          'ci_job_token_scope_enabled',
          'ci_separated_caches',
          'ci_allow_fork_pipelines_to_run_in_parent_project',
          'build_git_strategy',
          'keep_latest_artifact',
          'restrict_user_defined_variables',
          'runners_token',
          'runner_token_expiration_interval',
          'group_runners_enabled',
          'auto_cancel_pending_pipelines',
          'build_timeout',
          'auto_devops_enabled',
          'auto_devops_deploy_strategy',
          'import_error'
        )
      end
    end

    it_behaves_like 'storing arguments in the application context for the API' do
      let_it_be(:user) { create(:user) }
      let_it_be(:project) { create(:project, :public) }
      let(:expected_params) { { user: user.username, project: project.full_path } }

      subject { get api(path, user) }
    end

    describe 'repository_storage attribute' do
      let_it_be(:admin_mode) { false }

      before do
        get api(path, user, admin_mode: admin_mode)
      end

      context 'when authenticated as an admin' do
        let(:user) { create(:admin) }
        let_it_be(:admin_mode) { true }

        it 'returns repository_storage attribute' do
          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response['repository_storage']).to eq(project.repository_storage)
        end
      end

      context 'when authenticated as a regular user' do
        it 'does not return repository_storage attribute' do
          expect(json_response).not_to have_key('repository_storage')
        end
      end
    end

    it 'exposes service desk attributes' do
      get api(path, user)

      expect(json_response).to have_key 'service_desk_enabled'
      expect(json_response).to have_key 'service_desk_address'
    end

    context 'when project is shared to multiple groups' do
      it 'avoids N+1 queries', :use_sql_query_cache do
        create(:project_group_link, project: project)
        get api(path, user)
        expect(response).to have_gitlab_http_status(:ok)

        control = ActiveRecord::QueryRecorder.new do
          get api(path, user)
        end

        create(:project_group_link, project: project)

        expect do
          get api(path, user)
        end.not_to exceed_query_limit(control)
      end
    end
  end

  describe 'GET /projects/:id/users' do
    let(:path) { "/projects/#{project.id}/users" }

    shared_examples_for 'project users response' do
      let(:reporter_1) { create(:user) }
      let(:reporter_2) { create(:user) }

      before do
        project.add_reporter(reporter_1)
        project.add_reporter(reporter_2)
      end

      it 'returns the project users' do
        get api(path, current_user)

        user = project.namespace.first_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(3)

        first_user = json_response.first
        expect(first_user['username']).to eq(user.username)
        expect(first_user['name']).to eq(user.name)
        expect(first_user.keys).to include(*%w[name username id state avatar_url web_url])

        ids = json_response.map { |raw_user| raw_user['id'] }
        expect(ids).to eq([user.id, reporter_1.id, reporter_2.id])
      end
    end

    it_behaves_like 'GET request permissions for admin mode' do
      let(:failed_status_code) { :not_found }
    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(:project) { project4 }
          let(:current_user) { user4 }
        end
      end

      it 'returns a 404 error if not found' do
        get api("/projects/#{non_existing_record_id}/users", user)

        expect(response).to have_gitlab_http_status(:not_found)
        expect(json_response['message']).to eq('404 Project Not Found')
      end

      it 'returns a 404 error if user is not a member' do
        other_user = create(:user)

        get api(path, other_user)

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

      it 'filters out users listed in skip_users' do
        other_user = create(:user)
        project.team.add_developer(other_user)

        get api("/projects/#{project.id}/users?skip_users=#{user.id}", user)

        expect(response).to have_gitlab_http_status(:ok)
        expect(json_response.size).to eq(2)
        expect(json_response.map { |m| m['id'] }).not_to include(user.id)
      end
    end
  end

  describe 'fork management' do
    let_it_be_with_refind(:project_fork_target) { create(:project) }
    let_it_be_with_refind(:project_fork_source) { create(:project, :public) }
    let_it_be_with_refind(:private_project_fork_source) { create(:project, :private) }

    describe 'POST /projects/:id/fork/:forked_from_id' do
      let(:path) { "/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}" }

      it_behaves_like 'POST request permissions for admin mode' do
        let(:params) { {} }
        let(:failed_status_code) { :not_found }
      end

      context 'user is a developer' do
        before do
          project_fork_target.add_developer(user)
        end

        it 'denies project to be forked from an existing project' do
          post api(path, user)

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

      it 'refreshes the forks count cache' do
        expect(project_fork_source.forks_count).to be_zero
      end

      context 'user is maintainer' do
        before do
          project_fork_target.add_maintainer(user)
        end

        it 'allows project to be forked from an existing project' do
          expect(project_fork_target).not_to be_forked

          post api(path, user)
          project_fork_target.reload

          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
          expect(project_fork_target).to be_forked
        end

        it 'fails without permission from forked_from project' do
          project_fork_source.project_feature.update_attribute(:forking_access_level, ProjectFeature::PRIVATE)

          post api(path, user)

          expect(response).to have_gitlab_http_status(:forbidden)
          expect(project_fork_target.forked_from_project).to be_nil
          expect(project_fork_target.fork_network_member).not_to be_present
          expect(project_fork_target).not_to be_forked
        end

        it 'denies project to be forked from a private project' do
          post api("/projects/#{project_fork_target.id}/fork/#{private_project_fork_source.id}", user)

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

      context 'user is admin' do
        it 'allows project to be forked from an existing project' do
          expect(project_fork_target).not_to be_forked

          post api(path, admin, admin_mode: true)

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

        it 'allows project to be forked from a private project' do
          post api("/projects/#{project_fork_target.id}/fork/#{private_project_fork_source.id}", admin, admin_mode: true)

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

        it 'refreshes the forks count cachce' do
          expect do
            post api(path, admin, admin_mode: true)
          end.to change(project_fork_source, :forks_count).by(1)
        end

        it 'fails if forked_from project which does not exist' do
          post api("/projects/#{project_fork_target.id}/fork/#{non_existing_record_id}", admin, admin_mode: true)
          expect(response).to have_gitlab_http_status(:not_found)
        end

        it 'fails with 409 if already forked' do
          other_project_fork_source = create(:project, :public)

          Projects::ForkService.new(project_fork_source, admin).execute(project_fork_target)

          post api("/projects/#{project_fork_target.id}/fork/#{other_project_fork_source.id}", admin, admin_mode: true)
          project_fork_target.reload

          expect(response).to have_gitlab_http_status(:conflict)
          expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id)
          expect(project_fork_target).to be_forked
        end
      end
    end

    describe 'DELETE /projects/:id/fork' do
      let(:path) { "/projects/#{project_fork_target.id}/fork" }

      it "is not visible to users outside group" do
        delete api(path, user)
        expect(response).to have_gitlab_http_status(:not_found)
      end

      context 'when users belong to project group' do
        let(:project_fork_target) { create(:project, group: create(:group)) }

        before do
          project_fork_target.group.add_owner user
          project_fork_target.group.add_developer user2
        end

        context 'for a forked project' do
          before do
            post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin, admin_mode: true)
            project_fork_target.reload
            expect(project_fork_target.forked_from_project).to be_present
            expect(project_fork_target).to be_forked
          end

          it_behaves_like 'DELETE request permissions for admin mode' do
            let(:success_status_code) { :no_content }
            let(:failed_status_code) { :not_found }
          end

          it 'makes forked project unforked' do
            delete api(path, admin, admin_mode: true)

            expect(response).to have_gitlab_http_status(:no_content)
            project_fork_target.reload
            expect(project_fork_target.forked_from_project).to be_nil
            expect(project_fork_target).not_to be_forked
          end

          it_behaves_like '412 response' do
            subject(:request) { api(path, admin, admin_mode: true) }
          end
        end

        it 'is forbidden to non-owner users' do
          delete api(path, user2)
          expect(response).to have_gitlab_http_status(:forbidden)
        end

        it 'is idempotent if not forked' do
          expect(project_fork_target.forked_from_project).to be_nil
          delete api(path, admin, admin_mode: true)
          expect(response).to have_gitlab_http_status(:not_modified)
          expect(project_fork_target.reload.forked_from_project).to be_nil
        end
      end
    end

    describe 'GET /projects/:id/forks' do
      let_it_be_with_refind(:private_fork) { create(:project, :private, :empty_repo) }
      let_it_be(:member) { create(:user) }
      let_it_be(:non_member) { create(:user) }

      before_all do
        private_fork.add_developer(member)
      end

      context 'for a forked project' do
        before do
          post api("/projects/#{private_fork.id}/fork/#{project_fork_source.id}", admin, admin_mode: true)
          private_fork.reload
          expect(private_fork.forked_from_project).to be_present
          expect(private_fork).to be_forked
          project_fork_source.reload
          expect(project_fork_source.forks.length).to eq(1)
          expect(project_fork_source.forks).to include(private_fork)
        end

        context 'for a user that can access the forks' do
          it 'returns the forks' do
            get api("/projects/#{project_fork_source.id}/forks", member)

            expect(response).to have_gitlab_http_status(:ok)
            expect(response).to include_pagination_headers
            expect(json_response.length).to eq(1)
            expect(json_response[0]['name']).to eq(private_fork.name)
          end

          context 'filter by updated_at' do
            before do
              private_fork.update!(updated_at: 4.days.ago)
            end

            it 'returns only forks updated on the given timeframe' do
              get api("/projects/#{project_fork_source.id}/forks", member),
                params: { updated_before: 2.days.ago.iso8601, updated_after: 6.days.ago }

              expect(response).to have_gitlab_http_status(:ok)
              expect(json_response.map { |project| project['id'] }).to contain_exactly(private_fork.id)
            end
          end
        end

        context 'for a user that cannot access the forks' do
          it 'returns an empty array' do
            get api("/projects/#{project_fork_source.id}/forks", non_member)

            expect(response).to have_gitlab_http_status(:ok)
            expect(response).to include_pagination_headers
            expect(json_response.length).to eq(0)
          end
        end
      end

      context 'for a non-forked project' do
        it 'returns an empty array' do
          get api("/projects/#{project_fork_source.id}/forks")

          expect(response).to have_gitlab_http_status(:ok)
          expect(response).to include_pagination_headers
          expect(json_response.length).to eq(0)
        end
      end
    end
  end

  describe "POST /projects/:id/share" do
    let_it_be(:group) { create(:group, :private) }
    let_it_be(:group_user) { create(:user) }
    let(:path) { "/projects/#{project.id}/share" }

    before do
      group.add_developer(user)
      group.add_developer(group_user)
    end

    it "shares project with group" do
      expires_at = 10.days.from_now.to_date

      expect do
        post api(path, user), params: { group_id: group.id, group_access: Gitlab::Access::DEVELOPER, expires_at: expires_at }
      end.to change { ProjectGroupLink.count }.by(1)

      expect(response).to have_gitlab_http_status(:created)
      expect(json_response['group_id']).to eq(group.id)
      expect(json_response['group_access']).to eq(Gitlab::Access::DEVELOPER)
      expect(json_response['expires_at']).to eq(expires_at.to_s)
    end

    it 'updates project authorization', :sidekiq_inline do
      expect do
        post api(path, user), params: { group_id: group.id, group_access: Gitlab::Access::DEVELOPER }
      end.to(
        change { group_user.can?(:read_project, project) }.from(false).to(true)
      )
    end

    it "returns a 400 error when group id is not given" do
      post api(path, user), params: { group_access: Gitlab::Access::DEVELOPER }
      expect(response).to have_gitlab_http_status(:bad_request)
    end

    it "returns a 400 error when access level is not given" do
      post api(path, user), params: { group_id: group.id }
      expect(response).to have_gitlab_http_status(:bad_request)
    end

    it "returns a 400 error when sharing is disabled" do
      project.namespace.update!(share_with_group_lock: true)
      post api(path, user), params: { group_id: group.id, group_access: Gitlab::Access::DEVELOPER }
      expect(response).to have_gitlab_http_status(:bad_request)
    end

    it 'returns a 404 error when user cannot read group' do
      private_group = create(:group, :private)

      post api(path, user), params: { group_id: private_group.id, group_access: Gitlab::Access::DEVELOPER }

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

    it 'returns a 404 error when group does not exist' do
      post api(path, user), params: { group_id: non_existing_record_id, group_access: Gitlab::Access::DEVELOPER }

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

    it "returns a 400 error when wrong params passed" do
      post api(path, user), params: { group_id: group.id, group_access: non_existing_record_access_level }

      expect(response).to have_gitlab_http_status(:bad_request)
      expect(json_response['error']).to eq 'group_access does not have a valid value'
    end

    it "returns a 400 error when the project-group share is created with an OWNER access level" do
      post api(path, user), params: { group_id: group.id, group_access: Gitlab::Access::OWNER }

      expect(response).to have_gitlab_http_status(:bad_request)
      expect(json_response['error']).to eq 'group_access does not have a valid value'
    end

    it "returns a 409 error when link is not saved" do
      allow(::Projects::GroupLinks::CreateService).to receive_message_chain(:new, :execute)
        .and_return({ status: :error, http_status: 409, message: 'error' })

      post api(path, user), params: { group_id: group.id, group_access: Gitlab::Access::DEVELOPER }

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

    context 'when project is forked' do
      let(:forked_project) { fork_project(project) }
      let(:path) { "/projects/#{forked_project.id}/share" }

      it 'returns a 404 error when group does not exist' do
        forked_project.add_maintainer(user)
        post api(path, user), params: { group_id: non_existing_record_id, group_access: Gitlab::Access::DEVELOPER }

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

  describe 'DELETE /projects/:id/share/:group_id' do
    context 'for a valid group' do
      let_it_be(:group) { create(:group, :private) }
      let_it_be(:group_user) { create(:user) }

      before do
        group.add_developer(group_user)

        create(:project_group_link, group: group, project: project)
      end

      it 'returns 204 when deleting a group share' do
        delete api("/projects/#{project.id}/share/#{group.id}", user)

        expect(response).to have_gitlab_http_status(:no_content)
        expect(project.project_group_links).to be_empty
      end

      it 'updates project authorization', :sidekiq_inline do
        expect do
          delete api("/projects/#{project.id}/share/#{group.id}", user)
        end.to(
          change { group_user.can?(:read_project, project) }.from(true).to(false)
        )
      end

      it_behaves_like '412 response' do
        subject(:request) { api("/projects/#{project.id}/share/#{group.id}", user) }
      end
    end

    it 'returns a 400 when group id is not an integer' do
      delete api("/projects/#{project.id}/share/foo", user)

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

    it 'returns a 404 error when group link does not exist' do
      delete api("/projects/#{project.id}/share/#{non_existing_record_id}", user)

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

    it 'returns a 404 error when project does not exist' do
      delete api("/projects/#{non_existing_record_id}/share/#{non_existing_record_id}", user)

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

  describe 'POST /projects/:id/import_project_members/:project_id' do
    let_it_be(:project2) { create(:project) }
    let_it_be(:project2_user) { create(:user) }
    let(:path) { "/projects/#{project.id}/import_project_members/#{project2.id}" }

    before_all do
      project.add_maintainer(user)
      project2.add_maintainer(user)
      project2.add_developer(project2_user)
    end

    it 'records the query', :request_store, :use_sql_query_cache do
      post api(path, user)
      expect(response).to have_gitlab_http_status(:created)

      control_project = create(:project)
      control_project.add_maintainer(user)
      control_project.add_developer(create(:user))

      control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
        post api("/projects/#{project.id}/import_project_members/#{control_project.id}", user)
      end

      measure_project = create(:project)
      measure_project.add_maintainer(user)
      measure_project.add_developer(create(:user))
      measure_project.add_developer(create(:user)) # make this 2nd one to find any n+1

      unresolved_n_plus_ones = 27 # 27 queries added per member

      expect do
        post api("/projects/#{project.id}/import_project_members/#{measure_project.id}", user)
      end.not_to exceed_all_query_limit(control.count).with_threshold(unresolved_n_plus_ones)
    end

    it 'returns 200 when it successfully imports members from another project' do
      expect do
        post api(path, user)
      end.to change { project.members.count }.by(2)

      expect(response).to have_gitlab_http_status(:created)
      expect(json_response['message']).to eq('Successfully imported')
    end

    it 'returns 404 if the source project does not exist' do
      expect do
        post api("/projects/#{project.id}/import_project_members/#{non_existing_record_id}", user)
      end.not_to change { project.members.count }

      expect(response).to have_gitlab_http_status(:not_found)
      expect(json_response['message']).to eq('404 Project Not Found')
    end

    it 'returns 404 if the target project members cannot be administered by the requester' do
      private_project = create(:project, :private)

      expect do
        post api("/projects/#{private_project.id}/import_project_members/#{project2.id}", user)
      end.not_to change { project.members.count }

      expect(response).to have_gitlab_http_status(:not_found)
      expect(json_response['message']).to eq('404 Project Not Found')
    end

    it 'returns 404 if the source project members cannot be viewed by the requester' do
      private_project = create(:project, :private)

      expect do
        post api("/projects/#{project.id}/import_project_members/#{private_project.id}", user)
      end.not_to change { project.members.count }

      expect(response).to have_gitlab_http_status(:not_found)
      expect(json_response['message']).to eq('404 Project Not Found')
    end

    it 'returns 403 if the source project members cannot be administered by the requester' do
      project.add_maintainer(user2)
      project2.add_developer(user2)

      expect do
        post api(path, user2)
      end.not_to change { project.members.count }

      expect(response).to have_gitlab_http_status(:forbidden)
      expect(json_response['message']).to eq('403 Forbidden - Project')
    end

    it 'returns 422 if the import failed for valid projects' do
      allow_next_instance_of(::ProjectTeam) do |project_team|
        allow(project_team).to receive(:import).and_return(false)
      end

      expect do
        post api(path, user)
      end.not_to change { project.members.count }

      expect(response).to have_gitlab_http_status(:unprocessable_entity)
      expect(json_response['message']).to eq('Import failed')
    end
  end

  describe 'PUT /projects/:id' do
    let(:path) { "/projects/#{project.id}" }

    before do
      expect(project).to be_persisted
      expect(user).to be_persisted
      expect(user3).to be_persisted
      expect(user4).to be_persisted
      expect(project3).to be_persisted
      expect(project4).to be_persisted
      expect(project_member2).to be_persisted
      expect(project_member).to be_persisted
    end

    it_behaves_like 'PUT request permissions for admin mode' do
      let(:params) { { visibility: 'internal' } }
      let(:failed_status_code) { :not_found }
    end

    describe 'updating packages_enabled attribute' do
      it 'is enabled by default' do
        expect(project.packages_enabled).to be true
      end

      it 'disables project packages feature' do
        put(api(path, user), params: { packages_enabled: false })

        expect(response).to have_gitlab_http_status(:ok)
        expect(project.reload.packages_enabled).to be false
        expect(json_response['packages_enabled']).to eq(false)
      end
    end

    it 'sets container_registry_access_level' do
      put api(path, user), params: { container_registry_access_level: 'private' }

      expect(response).to have_gitlab_http_status(:ok)
      expect(json_response['container_registry_access_level']).to eq('private')
      expect(Project.find_by(path: project[:path]).container_registry_access_level).to eq(ProjectFeature::PRIVATE)
    end

    it 'sets container_registry_enabled' do
      project.project_feature.update!(container_registry_access_level: ProjectFeature::DISABLED)

      put(api(path, user), params: { container_registry_enabled: true })

      expect(response).to have_gitlab_http_status(:ok)
      expect(json_response['container_registry_enabled']).to eq(true)
      expect(project.reload.container_registry_access_level).to eq(ProjectFeature::ENABLED)
    end

    it 'sets security_and_compliance_access_level' do
      put api(path, user), params: { security_and_compliance_access_level: 'private' }

      expect(response).to have_gitlab_http_status(:ok)
      expect(json_response['security_and_compliance_access_level']).to eq('private')
      expect(Project.find_by(path: project[:path]).security_and_compliance_access_level).to eq(ProjectFeature::PRIVATE)
    end

    it 'sets analytics_access_level' do
      put api(path, user), params: { analytics_access_level: 'private' }

      expect(response).to have_gitlab_http_status(:ok)
      expect(json_response['analytics_access_level']).to eq('private')
      expect(Project.find_by(path: project[:path]).analytics_access_level).to eq(ProjectFeature::PRIVATE)
    end

    %i(releases_access_level environments_access_level feature_flags_access_level infrastructure_access_level monitor_access_level).each do |field|
      it "sets #{field}" do
        put api(path, user), params: { field => 'private' }

        expect(response).to have_gitlab_http_status(:ok)
        expect(json_response[field.to_s]).to eq('private')
        expect(Project.find_by(path: project[:path]).public_send(field)).to eq(ProjectFeature::PRIVATE)
      end
    end

    it 'returns 400 when nothing sent' do
      project_param = {}

      put api(path, user), params: project_param

      expect(response).to have_gitlab_http_status(:bad_request)
      expect(json_response['error']).to match('at least one parameter must be provided')
    end

    context 'when unauthenticated' do
      it 'returns authentication error' do
        project_param = { name: 'bar' }

        put api(path), params: project_param

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

    context 'when authenticated as project owner' do
      it 'updates visibility_level' do
        project_param = { visibility: 'public' }

        put api("/projects/#{project3.id}", user), params: project_param

        expect(response).to have_gitlab_http_status(:ok)

        project_param.each_pair do |k, v|
          expect(json_response[k.to_s]).to eq(v)
        end
      end

      it 'updates visibility_level from public to private' do
        project3.update!({ visibility_level: Gitlab::VisibilityLevel::PUBLIC })
        project_param = { visibility: 'private' }

        put api("/projects/#{project3.id}", user), params: project_param

        expect(response).to have_gitlab_http_status(:ok)

        project_param.each_pair do |k, v|
          expect(json_response[k.to_s]).to eq(v)
        end

        expect(json_response['visibility']).to eq('private')
      end

      it 'does not update visibility_level if it is restricted' do
        stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])

        put api("/projects/#{project3.id}", user), params: { visibility: 'internal' }

        expect(response).to have_gitlab_http_status(:bad_request)
        expect(json_response['message']['visibility_level']).to include('internal has been restricted by your GitLab administrator')
      end

      it 'does not update name to existing name' do
        project_param = { name: project3.name }

        put api(path, user), params: project_param

        expect(response).to have_gitlab_http_status(:bad_request)
        expect(json_response['message']['name']).to eq(['has already been taken'])
      end

      it 'updates request_access_enabled' do
        project_param = { request_access_enabled: false }

        put api(path, user), params: project_param

        expect(response).to have_gitlab_http_status(:ok)
        expect(json_response['request_access_enabled']).to eq(false)
      end

      it 'updates path & name to existing path & name in different namespace' do
        project_param = { path: project4.path, name: project4.name }

        put api("/projects/#{project3.id}", user), params: project_param

        expect(response).to have_gitlab_http_status(:ok)

        project_param.each_pair do |k, v|
          expect(json_response[k.to_s]).to eq(v)
        end
      end

      it 'updates default_branch' do
        project_param = { default_branch: 'something_else' }

        put api(path, user), params: project_param

        expect(response).to have_gitlab_http_status(:ok)

        project_param.each_pair do |k, v|
          expect(json_response[k.to_s]).to eq(v)
        end
      end

      it 'updates jobs_enabled' do
        project_param = { jobs_enabled: true }

        put api("/projects/#{project3.id}", user), params: project_param

        expect(response).to have_gitlab_http_status(:ok)

        project_param.each_pair do |k, v|
          expect(json_response[k.to_s]).to eq(v)
        end
      end

      it 'updates builds_access_level' do
        project_param = { builds_access_level: 'private' }

        put api("/projects/#{project3.id}", user), params: project_param

        expect(response).to have_gitlab_http_status(:ok)

        expect(json_response['builds_access_level']).to eq('private')
      end

      it 'updates pages_access_level' do
        project_param = { pages_access_level: 'private' }

        put api("/projects/#{project3.id}", user), params: project_param

        expect(response).to have_gitlab_http_status(:ok)

        expect(json_response['pages_access_level']).to eq('private')
      end

      it 'updates emails_disabled' do
        project_param = { emails_disabled: true }

        put api("/projects/#{project3.id}", user), params: project_param

        expect(response).to have_gitlab_http_status(:ok)

        expect(json_response['emails_disabled']).to eq(true)
      end

      it 'updates build_git_strategy' do
        project_param = { build_git_strategy: 'clone' }

        put api("/projects/#{project3.id}", user), params: project_param

        expect(response).to have_gitlab_http_status(:ok)

        expect(json_response['build_git_strategy']).to eq('clone')
      end

      it 'rejects to update build_git_strategy when build_git_strategy is invalid' do
        project_param = { build_git_strategy: 'invalid' }

        put api("/projects/#{project3.id}", user), params: project_param

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

      it 'updates merge_method' do
        project_param = { merge_method: 'ff' }

        put api("/projects/#{project3.id}", user), params: project_param

        expect(response).to have_gitlab_http_status(:ok)

        project_param.each_pair do |k, v|
          expect(json_response[k.to_s]).to eq(v)
        end
      end

      it 'rejects to update merge_method when merge_method is invalid' do
        project_param = { merge_method: 'invalid' }

        put api("/projects/#{project3.id}", user), params: project_param

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

      it 'updates restrict_user_defined_variables' do
        project_param = { restrict_user_defined_variables: true }

        put api("/projects/#{project3.id}", user), params: project_param

        expect(response).to have_gitlab_http_status(:ok)

        project_param.each_pair do |k, v|
          expect(json_response[k.to_s]).to eq(v)
        end
      end

      context 'with changes to the avatar' do
        let_it_be(:avatar_file) { fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') }
        let_it_be(:alternate_avatar_file) { fixture_file_upload('spec/fixtures/rails_sample.png', 'image/png') }
        let_it_be(:project_with_avatar, reload: true) do
          create(:project,
                 :private,
                 :repository,
                 name: 'project-with-avatar',
                 creator_id: user.id,
                 namespace: user.namespace,
                 avatar: avatar_file)
        end

        it 'uploads avatar to project without an avatar' do
          workhorse_form_with_file(
            api("/projects/#{project3.id}", user),
            method: :put,
            file_key: :avatar,
            params: { avatar: avatar_file }
          )

          aggregate_failures "testing response" do
            expect(response).to have_gitlab_http_status(:ok)
            expect(json_response['avatar_url']).to eq('http://localhost/uploads/' \
                                                      '-/system/project/avatar/' \
                                                      "#{project3.id}/banana_sample.gif")
          end
        end

        it 'uploads and changes avatar to project with an avatar' do
          workhorse_form_with_file(
            api("/projects/#{project_with_avatar.id}", user),
            method: :put,
            file_key: :avatar,
            params: { avatar: alternate_avatar_file }
          )

          aggregate_failures "testing response" do
            expect(response).to have_gitlab_http_status(:ok)
            expect(json_response['avatar_url']).to eq('http://localhost/uploads/' \
                                                      '-/system/project/avatar/' \
                                                      "#{project_with_avatar.id}/rails_sample.png")
          end
        end

        it 'uploads and changes avatar to project among other changes' do
          workhorse_form_with_file(
            api("/projects/#{project_with_avatar.id}", user),
            method: :put,
            file_key: :avatar,
            params: { description: 'changed description', avatar: avatar_file }
          )

          aggregate_failures "testing response" do
            expect(response).to have_gitlab_http_status(:ok)
            expect(json_response['description']).to eq('changed description')
            expect(json_response['avatar_url']).to eq('http://localhost/uploads/' \
                                                      '-/system/project/avatar/' \
                                                      "#{project_with_avatar.id}/banana_sample.gif")
          end
        end

        it 'removes avatar from project with an avatar' do
          put api("/projects/#{project_with_avatar.id}", user), params: { avatar: '' }

          aggregate_failures "testing response" do
            expect(response).to have_gitlab_http_status(:ok)
            expect(json_response['avatar_url']).to be_nil
            expect(project_with_avatar.reload.avatar_url).to be_nil
          end
        end
      end

      it 'updates auto_devops_deploy_strategy' do
        project_param = { auto_devops_deploy_strategy: 'timed_incremental' }

        put api("/projects/#{project3.id}", user), params: project_param

        expect(response).to have_gitlab_http_status(:ok)

        expect(json_response['auto_devops_deploy_strategy']).to eq('timed_incremental')
      end

      it 'updates auto_devops_enabled' do
        project_param = { auto_devops_enabled: false }

        put api("/projects/#{project3.id}", user), params: project_param

        expect(response).to have_gitlab_http_status(:ok)

        expect(json_response['auto_devops_enabled']).to eq(false)
      end

      it 'updates topics using tag_list (deprecated)' do
        project_param = { tag_list: 'topic1' }

        put api("/projects/#{project3.id}", user), params: project_param

        expect(response).to have_gitlab_http_status(:ok)

        expect(json_response['topics']).to eq(%w[topic1])
      end

      it 'updates topics' do
        project_param = { topics: 'topic2' }

        put api("/projects/#{project3.id}", user), params: project_param

        expect(response).to have_gitlab_http_status(:ok)

        expect(json_response['topics']).to eq(%w[topic2])
      end

      it 'updates enforce_auth_checks_on_uploads' do
        project3.update!(enforce_auth_checks_on_uploads: false)

        project_param = { enforce_auth_checks_on_uploads: true }

        expect { put api("/projects/#{project3.id}", user), params: project_param }
          .to change { project3.reload.enforce_auth_checks_on_uploads }
          .from(false)
          .to(true)

        expect(response).to have_gitlab_http_status(:ok)
        expect(json_response['enforce_auth_checks_on_uploads']).to eq(true)
      end

      it 'updates squash_option' do
        project3.update!(squash_option: 'always')

        project_param = { squash_option: "default_on" }

        expect { put api("/projects/#{project3.id}", user), params: project_param }
          .to change { project3.reload.squash_option }
          .from('always')
          .to('default_on')

        expect(response).to have_gitlab_http_status(:ok)
        expect(json_response['squash_option']).to eq("default_on")
      end

      it 'does not update an invalid squash_option' do
        project_param = { squash_option: "jawn" }

        expect { put api("/projects/#{project3.id}", user), params: project_param }
          .not_to change { project3.reload.squash_option }

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

    context 'when authenticated as project maintainer' do
      it 'updates path' do
        project_param = { path: 'bar' }
        put api("/projects/#{project3.id}", user4), params: project_param
        expect(response).to have_gitlab_http_status(:ok)
        project_param.each_pair do |k, v|
          expect(json_response[k.to_s]).to eq(v)
        end
      end

      it 'updates other attributes' do
        project_param = { issues_enabled: true,
                          wiki_enabled: true,
                          snippets_enabled: true,
                          merge_requests_enabled: true,
                          merge_method: 'ff',
                          ci_default_git_depth: 20,
                          ci_forward_deployment_enabled: false,
                          ci_allow_fork_pipelines_to_run_in_parent_project: false,
                          ci_separated_caches: false,
                          description: 'new description' }

        put api("/projects/#{project3.id}", user4), params: project_param
        expect(response).to have_gitlab_http_status(:ok)
        project_param.each_pair do |k, v|
          expect(json_response[k.to_s]).to eq(v)
        end
      end

      it 'does not update path to existing path' do
        project_param = { path: project.path }
        put api("/projects/#{project3.id}", user4), params: project_param
        expect(response).to have_gitlab_http_status(:bad_request)
        expect(json_response['message']['path']).to eq(['has already been taken'])
      end

      it 'updates name' do
        project_param = { name: 'bar' }

        put api(path, user), params: project_param

        expect(response).to have_gitlab_http_status(:ok)

        project_param.each_pair do |k, v|
          expect(json_response[k.to_s]).to eq(v)
        end
      end

      it 'does not update visibility_level' do
        project_param = { visibility: 'public' }
        put api("/projects/#{project3.id}", user4), params: project_param
        expect(response).to have_gitlab_http_status(:forbidden)
      end

      it 'updates container_expiration_policy' do
        project_param = {
          container_expiration_policy_attributes: {
            cadence: '1month',
            keep_n: 1,
            name_regex_keep: 'foo.*'
          }
        }

        put api("/projects/#{project3.id}", user4), params: project_param

        expect(response).to have_gitlab_http_status(:ok)

        expect(json_response['container_expiration_policy']['cadence']).to eq('1month')
        expect(json_response['container_expiration_policy']['keep_n']).to eq(1)
        expect(json_response['container_expiration_policy']['name_regex_keep']).to eq('foo.*')
      end

      it "doesn't update container_expiration_policy with invalid regex" do
        project_param = {
          container_expiration_policy_attributes: {
            cadence: '1month',
            enabled: true,
            keep_n: 1,
            name_regex_keep: '['
          }
        }

        put api("/projects/#{project3.id}", user4), params: project_param

        expect(response).to have_gitlab_http_status(:bad_request)
        expect(json_response['message']['container_expiration_policy.name_regex_keep']).to contain_exactly('not valid RE2 syntax: missing ]: [')
      end

      it "doesn't update container_expiration_policy with invalid keep_n" do
        project_param = {
          container_expiration_policy_attributes: {
            cadence: '1month',
            enabled: true,
            keep_n: 'not_int',
            name_regex_keep: 'foo.*'
          }
        }

        put api("/projects/#{project3.id}", user4), params: project_param

        expect(response).to have_gitlab_http_status(:bad_request)
        expect(json_response['error']).to eq('container_expiration_policy_attributes[keep_n] is invalid')
      end
    end

    context 'when authenticated as project developer' do
      it 'does not update other attributes' do
        project_param = { path: 'bar',
                          issues_enabled: true,
                          wiki_enabled: true,
                          snippets_enabled: true,
                          merge_requests_enabled: true,
                          description: 'new description',
                          request_access_enabled: true }
        put api(path, user3), params: project_param
        expect(response).to have_gitlab_http_status(:forbidden)
      end
    end

    context 'when authenticated as the admin' do
      let_it_be(:admin) { create(:admin) }

      it 'ignores visibility level restrictions' do
        stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])

        put api("/projects/#{project3.id}", admin, admin_mode: true), params: { visibility: 'internal' }

        expect(response).to have_gitlab_http_status(:ok)
        expect(json_response['visibility']).to eq('internal')
      end
    end

    context 'when updating repository storage' do
      let(:unknown_storage) { 'new-storage' }
      let(:new_project) { create(:project, :repository, namespace: user.namespace) }

      context 'as a user' do
        it 'returns 200 but does not change repository_storage' do
          expect do
            Sidekiq::Testing.fake! do
              put(api("/projects/#{new_project.id}", user), params: { repository_storage: unknown_storage, issues_enabled: false })
            end
          end.not_to change(Projects::UpdateRepositoryStorageWorker.jobs, :size)

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response['issues_enabled']).to eq(false)
          expect(new_project.reload.repository.storage).to eq('default')
        end
      end

      context 'as an admin' do
        include_context 'custom session'

        let(:admin) { create(:admin) }

        it 'returns 400 when repository storage is unknown' do
          put(api("/projects/#{new_project.id}", admin, admin_mode: true), params: { repository_storage: unknown_storage })

          expect(response).to have_gitlab_http_status(:bad_request)
          expect(json_response['message']['repository_storage_moves']).to eq(['is invalid'])
        end

        it 'returns 200 when repository storage has changed' do
          stub_storage_settings('test_second_storage' => { 'path' => TestEnv::SECOND_STORAGE_PATH })

          expect do
            Sidekiq::Testing.fake! do
              put(api("/projects/#{new_project.id}", admin, admin_mode: true), params: { repository_storage: 'test_second_storage' })
            end
          end.to change(Projects::UpdateRepositoryStorageWorker.jobs, :size).by(1)

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

    context 'when updating service desk' do
      let(:params) { { service_desk_enabled: true } }

      subject(:request) { put(api(path, user), params: params) }

      before do
        project.update!(service_desk_enabled: false)

        allow(::Gitlab::Email::IncomingEmail).to receive(:enabled?).and_return(true)
      end

      it 'returns 200' do
        request

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

      it 'enables the service_desk' do
        expect { request }.to change { project.reload.service_desk_enabled }.to(true)
      end
    end

    context 'when updating keep latest artifact' do
      subject(:request) { put(api(path, user), params: { keep_latest_artifact: true }) }

      before do
        project.update!(keep_latest_artifact: false)
      end

      it 'returns 200' do
        request

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

      it 'enables keep_latest_artifact' do
        expect { request }.to change { project.reload.keep_latest_artifact }.to(true)
      end
    end

    context 'attribute mr_default_target_self' do
      let_it_be(:source_project) { create(:project, :public) }

      let(:forked_project) { fork_project(source_project, user) }

      it 'is by default set to false' do
        expect(source_project.mr_default_target_self).to be false
        expect(forked_project.mr_default_target_self).to be false
      end

      describe 'for a non-forked project' do
        before_all do
          source_project.add_maintainer(user)
        end

        it 'is not exposed' do
          get api("/projects/#{source_project.id}", user)

          expect(json_response).not_to include('mr_default_target_self')
        end

        it 'is not possible to update' do
          put api("/projects/#{source_project.id}", user), params: { mr_default_target_self: true }

          source_project.reload
          expect(source_project.mr_default_target_self).to be false
          expect(response).to have_gitlab_http_status(:bad_request)
        end
      end

      describe 'for a forked project' do
        it 'updates to true' do
          put api("/projects/#{forked_project.id}", user), params: { mr_default_target_self: true }

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response['mr_default_target_self']).to eq(true)
        end
      end
    end
  end

  describe 'POST /projects/:id/archive' do
    let(:path) { "/projects/#{project.id}/archive" }

    context 'on an unarchived project' do
      it 'archives the project' do
        post api(path, user)

        expect(response).to have_gitlab_http_status(:created)
        expect(json_response['archived']).to be_truthy
      end
    end

    context 'on an archived project' do
      before do
        ::Projects::UpdateService.new(project, user, archived: true).execute
      end

      it 'remains archived' do
        post api(path, user)

        expect(response).to have_gitlab_http_status(:created)
        expect(json_response['archived']).to be_truthy
      end
    end

    context 'user without archiving rights to the project' do
      before do
        project.add_developer(user3)
      end

      it 'rejects the action' do
        post api(path, user3)

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

  describe 'POST /projects/:id/unarchive' do
    let(:path) { "/projects/#{project.id}/unarchive" }

    context 'on an unarchived project' do
      it 'remains unarchived' do
        post api(path, user)

        expect(response).to have_gitlab_http_status(:created)
        expect(json_response['archived']).to be_falsey
      end
    end

    context 'on an archived project' do
      before do
        ::Projects::UpdateService.new(project, user, archived: true).execute
      end

      it 'unarchives the project' do
        post api(path, user)

        expect(response).to have_gitlab_http_status(:created)
        expect(json_response['archived']).to be_falsey
      end
    end

    context 'user without archiving rights to the project' do
      before do
        project.add_developer(user3)
      end

      it 'rejects the action' do
        post api(path, user3)

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

  describe 'POST /projects/:id/star' do
    let(:path) { "/projects/#{project.id}/star" }

    context 'on an unstarred project' do
      it 'stars the project' do
        expect { post api(path, user) }.to change { project.reload.star_count }.by(1)

        expect(response).to have_gitlab_http_status(:created)
        expect(json_response['star_count']).to eq(1)
      end
    end

    context 'on a starred project' do
      before do
        user.toggle_star(project)
        project.reload
      end

      it 'does not modify the star count' do
        expect { post api(path, user) }.not_to change { project.reload.star_count }

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

  describe 'POST /projects/:id/unstar' do
    let(:path) { "/projects/#{project.id}/unstar" }

    context 'on a starred project' do
      before do
        user.toggle_star(project)
        project.reload
      end

      it 'unstars the project' do
        expect { post api(path, user) }.to change { project.reload.star_count }.by(-1)

        expect(response).to have_gitlab_http_status(:created)
        expect(json_response['star_count']).to eq(0)
      end
    end

    context 'on an unstarred project' do
      it 'does not modify the star count' do
        expect { post api(path, user) }.not_to change { project.reload.star_count }

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

  describe 'GET /projects/:id/starrers' do
    let(:path) { "/projects/#{public_project.id}/starrers" }
    let(:public_project) { create(:project, :public) }
    let(:private_user) { create(:user, private_profile: true) }

    shared_examples_for 'project starrers response' do
      it 'returns an array of starrers' do
        get api(path, current_user)

        expect(response).to have_gitlab_http_status(:ok)
        expect(response).to include_pagination_headers
        expect(json_response).to be_an Array
        expect(json_response[0]['starred_since']).to be_present
        expect(json_response[0]['user']).to be_present
      end

      it 'returns the proper security headers' do
        get api(path, current_user)

        expect(response).to include_security_headers
      end
    end

    before do
      user.update!(starred_projects: [public_project])
      private_user.update!(starred_projects: [public_project])
    end

    it 'returns not_found(404) for not existing project' do
      get api("/projects/#{non_existing_record_id}/starrers", user)

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

    context 'public project without user' do
      it_behaves_like 'project starrers response' do
        let(:current_user) { nil }
      end

      it 'returns only starrers with a public profile' do
        get api(path, nil)

        user_ids = json_response.map { |s| s['user']['id'] }
        expect(user_ids).to include(user.id)
        expect(user_ids).not_to include(private_user.id)
      end
    end

    context 'public project with user with private profile' do
      it_behaves_like 'project starrers response' do
        let(:current_user) { private_user }
      end

      it 'returns current user with a private profile' do
        get api(path, private_user)

        user_ids = json_response.map { |s| s['user']['id'] }
        expect(user_ids).to include(user.id, private_user.id)
      end
    end

    context 'private project' do
      context 'with unauthorized user' do
        it 'returns not_found for existing but unauthorized project' do
          get api("/projects/#{project3.id}/starrers", user3)

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

      context 'without user' do
        it 'returns not_found for existing but unauthorized project' do
          get api("/projects/#{project3.id}/starrers", nil)

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

  describe 'GET /projects/:id/languages' do
    context 'with an authorized user' do
      it_behaves_like 'languages and percentages JSON response' do
        let(:project) { project3 }
      end

      it 'returns not_found(404) for not existing project' do
        get api("/projects/#{non_existing_record_id}/languages", user)

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

    context 'with not authorized user' do
      it 'returns not_found for existing but unauthorized project' do
        get api("/projects/#{project3.id}/languages", user3)

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

    context 'without user' do
      let(:project_public) { create(:project, :public, :repository) }

      it_behaves_like 'languages and percentages JSON response' do
        let(:project) { project_public }
      end

      it 'returns not_found for existing but unauthorized project' do
        get api("/projects/#{project3.id}/languages", nil)

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

  describe 'DELETE /projects/:id' do
    let(:path) { "/projects/#{project.id}" }

    it_behaves_like 'DELETE request permissions for admin mode' do
      let(:success_status_code) { :accepted }
      let(:failed_status_code) { :not_found }
    end

    context 'when authenticated as user' do
      it 'removes project' do
        delete api(path, user)

        expect(response).to have_gitlab_http_status(:accepted)
        expect(json_response['message']).to eql('202 Accepted')
      end

      it_behaves_like '412 response' do
        let(:success_status) { 202 }
        subject(:request) { api(path, user) }
      end

      it 'does not remove a project if not an owner' do
        user3 = create(:user)
        project.add_developer(user3)
        delete api(path, user3)
        expect(response).to have_gitlab_http_status(:forbidden)
      end

      it 'does not remove a non existing project' do
        delete api("/projects/#{non_existing_record_id}", user)
        expect(response).to have_gitlab_http_status(:not_found)
      end

      it 'does not remove a project not attached to user' do
        delete api(path, user2)
        expect(response).to have_gitlab_http_status(:not_found)
      end
    end

    context 'when authenticated as admin' do
      it 'removes any existing project' do
        delete api("/projects/#{project.id}", admin, admin_mode: true)

        expect(response).to have_gitlab_http_status(:accepted)
        expect(json_response['message']).to eql('202 Accepted')
      end

      it 'does not remove a non existing project' do
        delete api("/projects/#{non_existing_record_id}", admin, admin_mode: true)
        expect(response).to have_gitlab_http_status(:not_found)
      end

      it_behaves_like '412 response' do
        let(:success_status) { 202 }
        subject(:request) { api("/projects/#{project.id}", admin, admin_mode: true) }
      end
    end
  end

  describe 'POST /projects/:id/fork' do
    let(:project) do
      create(:project, :repository, creator: user, namespace: user.namespace)
    end

    let(:path) { "/projects/#{project.id}/fork" }

    let(:project2) do
      create(:project, :repository, creator: user, namespace: user.namespace)
    end

    let(:group) { create(:group, :public) }
    let(:group2) { create(:group, name: 'group2_name') }
    let(:group3) { create(:group, name: 'group3_name', parent: group2) }

    before do
      group.add_guest(user2)
      group2.add_maintainer(user2)
      group3.add_owner(user2)
      project.add_reporter(user2)
      project2.add_reporter(user2)
    end

    it_behaves_like 'POST request permissions for admin mode' do
      let(:params) { {} }
      let(:failed_status_code) { :not_found }
    end

    context 'when authenticated' do
      it 'forks if user has sufficient access to project' do
        post api(path, user2)

        expect(response).to have_gitlab_http_status(:created)
        expect(json_response['name']).to eq(project.name)
        expect(json_response['path']).to eq(project.path)
        expect(json_response['owner']['id']).to eq(user2.id)
        expect(json_response['namespace']['id']).to eq(user2.namespace.id)
        expect(json_response['forked_from_project']['id']).to eq(project.id)
        expect(json_response['import_status']).to eq('scheduled')
        expect(json_response).to include("import_error")
      end

      it 'forks if user is admin' do
        post api(path, admin, admin_mode: true)

        expect(response).to have_gitlab_http_status(:created)
        expect(json_response['name']).to eq(project.name)
        expect(json_response['path']).to eq(project.path)
        expect(json_response['owner']['id']).to eq(admin.id)
        expect(json_response['namespace']['id']).to eq(admin.namespace.id)
        expect(json_response['forked_from_project']['id']).to eq(project.id)
        expect(json_response['import_status']).to eq('scheduled')
        expect(json_response).to include("import_error")
      end

      it 'fails on missing project access for the project to fork' do
        new_user = create(:user)
        post api(path, new_user)

        expect(response).to have_gitlab_http_status(:not_found)
        expect(json_response['message']).to eq('404 Project Not Found')
      end

      it 'fails if forked project exists in the user namespace' do
        new_project = create(:project, name: project.name, path: project.path)
        new_project.add_reporter(user)

        post api("/projects/#{new_project.id}/fork", user)

        expect(response).to have_gitlab_http_status(:conflict)
        expect(json_response['message']['name']).to eq(['has already been taken'])
        expect(json_response['message']['path']).to eq(['has already been taken'])
      end

      it 'fails if project to fork from does not exist' do
        post api("/projects/#{non_existing_record_id}/fork", user)

        expect(response).to have_gitlab_http_status(:not_found)
        expect(json_response['message']).to eq('404 Project Not Found')
      end

      it 'forks with explicit own user namespace id' do
        post api(path, user2), params: { namespace: user2.namespace.id }

        expect(response).to have_gitlab_http_status(:created)
        expect(json_response['owner']['id']).to eq(user2.id)
      end

      it 'forks with explicit own user name as namespace' do
        post api(path, user2), params: { namespace: user2.username }

        expect(response).to have_gitlab_http_status(:created)
        expect(json_response['owner']['id']).to eq(user2.id)
      end

      it 'forks to another user when admin' do
        post api(path, admin, admin_mode: true), params: { namespace: user2.username }

        expect(response).to have_gitlab_http_status(:created)
        expect(json_response['owner']['id']).to eq(user2.id)
      end

      it 'fails if trying to fork to another user when not admin' do
        post api(path, user2), params: { namespace: admin.namespace.id }

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

      it 'fails if trying to fork to non-existent namespace' do
        post api(path, user2), params: { namespace: non_existing_record_id }

        expect(response).to have_gitlab_http_status(:not_found)
        expect(json_response['message']).to eq('404 Namespace Not Found')
      end

      it 'forks to owned group' do
        post api(path, user2), params: { namespace: group2.name }

        expect(response).to have_gitlab_http_status(:created)
        expect(json_response['namespace']['name']).to eq(group2.name)
      end

      context 'when namespace_id is specified' do
        shared_examples_for 'forking to specified namespace_id' do
          it 'forks to specified namespace_id' do
            expect(response).to have_gitlab_http_status(:created)
            expect(json_response['owner']['id']).to eq(user2.id)
            expect(json_response['namespace']['id']).to eq(user2.namespace.id)
          end
        end

        context 'and namespace_id is specified alone' do
          before do
            post api(path, user2), params: { namespace_id: user2.namespace.id }
          end

          it_behaves_like 'forking to specified namespace_id'
        end

        context 'and namespace_id and namespace are both specified' do
          before do
            post api(path, user2), params: { namespace_id: user2.namespace.id, namespace: admin.namespace.id }
          end

          it_behaves_like 'forking to specified namespace_id'
        end

        context 'and namespace_id and namespace_path are both specified' do
          before do
            post api(path, user2), params: { namespace_id: user2.namespace.id, namespace_path: admin.namespace.path }
          end

          it_behaves_like 'forking to specified namespace_id'
        end
      end

      context 'when namespace_path is specified' do
        shared_examples_for 'forking to specified namespace_path' do
          it 'forks to specified namespace_path' do
            expect(response).to have_gitlab_http_status(:created)
            expect(json_response['owner']['id']).to eq(user2.id)
            expect(json_response['namespace']['path']).to eq(user2.namespace.path)
          end
        end

        context 'and namespace_path is specified alone' do
          before do
            post api(path, user2), params: { namespace_path: user2.namespace.path }
          end

          it_behaves_like 'forking to specified namespace_path'
        end

        context 'and namespace_path and namespace are both specified' do
          before do
            post api(path, user2), params: { namespace_path: user2.namespace.path, namespace: admin.namespace.path }
          end

          it_behaves_like 'forking to specified namespace_path'
        end
      end

      it 'forks to owned subgroup' do
        full_path = "#{group2.path}/#{group3.path}"
        post api(path, user2), params: { namespace: full_path }

        expect(response).to have_gitlab_http_status(:created)
        expect(json_response['namespace']['name']).to eq(group3.name)
        expect(json_response['namespace']['full_path']).to eq(full_path)
      end

      it 'fails to fork to not owned group' do
        post api(path, user2), params: { namespace: group.name }

        expect(response).to have_gitlab_http_status(:not_found)
        expect(json_response['message']).to eq("404 Target Namespace Not Found")
      end

      it 'forks to not owned group when admin' do
        post api(path, admin, admin_mode: true), params: { namespace: group.name }

        expect(response).to have_gitlab_http_status(:created)
        expect(json_response['namespace']['name']).to eq(group.name)
      end

      it 'accepts a path for the target project' do
        post api(path, user2), params: { path: 'foobar' }

        expect(response).to have_gitlab_http_status(:created)
        expect(json_response['name']).to eq(project.name)
        expect(json_response['path']).to eq('foobar')
        expect(json_response['owner']['id']).to eq(user2.id)
        expect(json_response['namespace']['id']).to eq(user2.namespace.id)
        expect(json_response['forked_from_project']['id']).to eq(project.id)
        expect(json_response['import_status']).to eq('scheduled')
        expect(json_response).to include("import_error")
      end

      it 'fails to fork if path is already taken' do
        post api(path, user2), params: { path: 'foobar' }
        post api("/projects/#{project2.id}/fork", user2), params: { path: 'foobar' }

        expect(response).to have_gitlab_http_status(:conflict)
        expect(json_response['message']['path']).to eq(['has already been taken'])
      end

      it 'accepts custom parameters for the target project' do
        post api(path, user2),
          params: {
            name: 'My Random Project',
            description: 'A description',
            visibility: 'private',
            mr_default_target_self: true
          }

        expect(response).to have_gitlab_http_status(:created)
        expect(json_response['name']).to eq('My Random Project')
        expect(json_response['path']).to eq(project.path)
        expect(json_response['owner']['id']).to eq(user2.id)
        expect(json_response['namespace']['id']).to eq(user2.namespace.id)
        expect(json_response['forked_from_project']['id']).to eq(project.id)
        expect(json_response['description']).to eq('A description')
        expect(json_response['visibility']).to eq('private')
        expect(json_response['import_status']).to eq('scheduled')
        expect(json_response['mr_default_target_self']).to eq(true)
        expect(json_response).to include("import_error")
      end

      it 'fails to fork if name is already taken' do
        post api(path, user2), params: { name: 'My Random Project' }
        post api("/projects/#{project2.id}/fork", user2), params: { name: 'My Random Project' }

        expect(response).to have_gitlab_http_status(:conflict)
        expect(json_response['message']['name']).to eq(['has already been taken'])
      end

      it 'forks to the same namespace with alternative path and name' do
        post api(path, user), params: { path: 'path_2', name: 'name_2' }

        expect(response).to have_gitlab_http_status(:created)
        expect(json_response['name']).to eq('name_2')
        expect(json_response['path']).to eq('path_2')
        expect(json_response['owner']['id']).to eq(user.id)
        expect(json_response['namespace']['id']).to eq(user.namespace.id)
        expect(json_response['forked_from_project']['id']).to eq(project.id)
        expect(json_response['import_status']).to eq('scheduled')
      end

      it 'fails to fork to the same namespace without alternative path and name' do
        post api(path, user)

        expect(response).to have_gitlab_http_status(:conflict)
        expect(json_response['message']['path']).to eq(['has already been taken'])
        expect(json_response['message']['name']).to eq(['has already been taken'])
      end

      it 'fails to fork with an unknown visibility level' do
        post api(path, user2), params: { visibility: 'something' }

        expect(response).to have_gitlab_http_status(:bad_request)
        expect(json_response['error']).to eq('visibility does not have a valid value')
      end
    end

    context 'when unauthenticated' do
      it 'returns authentication error' do
        post api(path)

        expect(response).to have_gitlab_http_status(:unauthorized)
        expect(json_response['message']).to eq('401 Unauthorized')
      end
    end

    context 'forking disabled' do
      before do
        project.project_feature.update_attribute(
          :forking_access_level, ProjectFeature::DISABLED)
      end

      it 'denies project to be forked' do
        post api(path, admin, admin_mode: true)

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

  describe 'POST /projects/:id/housekeeping' do
    let(:housekeeping) { Repositories::HousekeepingService.new(project) }
    let(:params) { {} }
    let(:path) { "/projects/#{project.id}/housekeeping" }

    subject(:request) { post api(path, user), params: params }

    before do
      allow(Repositories::HousekeepingService).to receive(:new).with(project, :eager).and_return(housekeeping)
    end

    context 'when authenticated as owner' do
      it 'starts the housekeeping process' do
        expect(housekeeping).to receive(:execute).once

        request

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

      it 'logs an audit event' do
        expect(housekeeping).to receive(:execute).once.and_yield
        expect(::Gitlab::Audit::Auditor).to receive(:audit).with(a_hash_including(
          name: 'manually_trigger_housekeeping',
          author: user,
          scope: project,
          target: project,
          message: "Housekeeping task: eager"
        ))

        request
      end

      context 'when requesting prune' do
        let(:params) { { task: :prune } }

        it 'triggers a prune' do
          expect(Repositories::HousekeepingService).to receive(:new).with(project, :prune).and_return(housekeeping)
          expect(housekeeping).to receive(:execute).once

          request

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

      context 'when requesting an unsupported task' do
        let(:params) { { task: :unsupported_task } }

        it 'responds with bad_request' do
          expect(Repositories::HousekeepingService).not_to receive(:new)

          request

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

      context 'when housekeeping lease is taken' do
        it 'returns conflict' do
          expect(housekeeping).to receive(:execute).once.and_raise(Repositories::HousekeepingService::LeaseTaken)

          request

          expect(response).to have_gitlab_http_status(:conflict)
          expect(json_response['message']).to match(/Somebody already triggered housekeeping for this resource/)
        end
      end
    end

    context 'when authenticated as developer' do
      before do
        project_member
      end

      it 'returns forbidden error' do
        post api(path, user3)

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

    context 'when unauthenticated' do
      it 'returns authentication error' do
        post api(path)

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

  describe 'POST /projects/:id/repository_size' do
    let(:update_statistics_service) { Projects::UpdateStatisticsService.new(project, nil, statistics: [:repository_size, :lfs_objects_size]) }
    let(:path) { "/projects/#{project.id}/repository_size" }

    before do
      allow(Projects::UpdateStatisticsService).to receive(:new).with(project, nil, statistics: [:repository_size, :lfs_objects_size]).and_return(update_statistics_service)
    end

    context 'when authenticated as owner' do
      it 'starts the housekeeping process' do
        expect(update_statistics_service).to receive(:execute).once

        post api(path, user)

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

    context 'when authenticated as developer' do
      before do
        project_member
      end

      it 'returns forbidden error' do
        post api(path, user3)

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

    context 'when unauthenticated' do
      it 'returns authentication error' do
        post api(path)

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

  describe 'PUT /projects/:id/transfer' do
    let(:path) { "/projects/#{project.id}/transfer" }

    context 'when authenticated as owner' do
      let(:group) { create :group }

      it 'transfers the project to the new namespace' do
        group.add_owner(user)

        put api(path, user), params: { namespace: group.id }

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

      it 'fails when transferring to a non owned namespace' do
        put api(path, user), params: { namespace: group.id }

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

      it 'fails when transferring to an unknown namespace' do
        put api(path, user), params: { namespace: 'unknown' }

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

      it 'fails on missing namespace' do
        put api(path, user)

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

    context 'when authenticated as developer' do
      before do
        group.add_developer(user)
      end

      context 'target namespace allows developers to create projects' do
        let(:group) { create(:group, project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) }

        it 'fails transferring the project to the target namespace' do
          put api(path, user), params: { namespace: group.id }

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

  describe 'GET /projects/:id/transfer_locations' do
    let_it_be(:user) { create(:user) }
    let_it_be(:source_group) { create(:group) }
    let_it_be(:project) { create(:project, group: source_group) }

    let(:params) { {} }

    subject(:request) do
      get api("/projects/#{project.id}/transfer_locations", user), params: params
    end

    context 'when the user has rights to transfer the project' do
      let_it_be(:guest_group) { create(:group) }
      let_it_be(:maintainer_group) { create(:group, name: 'maintainer group', path: 'maintainer-group') }
      let_it_be(:owner_group) { create(:group, name: 'owner group', path: 'owner-group') }

      before do
        source_group.add_owner(user)
        guest_group.add_guest(user)
        maintainer_group.add_maintainer(user)
        owner_group.add_owner(user)
      end

      it 'returns 200' do
        request

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

      it 'includes groups where the user has permissions to transfer a project to' do
        request

        expect(project_ids_from_response).to include(maintainer_group.id, owner_group.id)
      end

      it 'does not include groups where the user doesn not have permissions to transfer a project' do
        request

        expect(project_ids_from_response).not_to include(guest_group.id)
      end

      context 'with search' do
        let(:params) { { search: 'maintainer' } }

        it 'includes groups where the user has permissions to transfer a project to' do
          request

          expect(project_ids_from_response).to contain_exactly(maintainer_group.id)
        end
      end

      context 'group shares' do
        let_it_be(:shared_to_owner_group) { create(:group) }
        let_it_be(:shared_to_guest_group) { create(:group) }

        before do
          create(:group_group_link, :owner,
                 shared_with_group: owner_group,
                 shared_group: shared_to_owner_group
          )

          create(:group_group_link, :guest,
                 shared_with_group: guest_group,
                 shared_group: shared_to_guest_group
          )
        end

        it 'only includes groups arising from group shares where the user has permission to transfer a project to' do
          request

          expect(project_ids_from_response).to include(shared_to_owner_group.id)
          expect(project_ids_from_response).not_to include(shared_to_guest_group.id)
        end
      end

      def project_ids_from_response
        json_response.map { |project| project['id'] }
      end
    end

    context 'when the user does not have permissions to transfer the project' do
      before do
        source_group.add_developer(user)
      end

      it 'returns 403' do
        request

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

  describe 'GET /projects/:id/storage' do
    let(:path) { "/projects/#{project.id}/storage" }

    it_behaves_like 'GET request permissions for admin mode'

    context 'when unauthenticated' do
      it 'does not return project storage data' do
        get api(path)

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

    it 'returns project storage data when user is admin' do
      get api(path, create(:admin), admin_mode: true)

      expect(response).to have_gitlab_http_status(:ok)
      expect(json_response['project_id']).to eq(project.id)
      expect(json_response['disk_path']).to eq(project.repository.disk_path)
      expect(json_response['created_at']).to be_present
      expect(json_response['repository_storage']).to eq(project.repository_storage)
    end

    it 'does not return project storage data when user is not admin' do
      get api(path, user3)

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

    it 'responds with a 401 for unauthenticated users trying to access a non-existent project id' do
      expect(Project.find_by(id: non_existing_record_id)).to be_nil

      get api("/projects/#{non_existing_record_id}/storage")

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

    it 'responds with a 403 for non-admin users trying to access a non-existent project id' do
      expect(Project.find_by(id: non_existing_record_id)).to be_nil

      get api("/projects/#{non_existing_record_id}/storage", user3)

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

  it_behaves_like 'custom attributes endpoints', 'projects' do
    let(:attributable) { project }
    let(:other_attributable) { project2 }
  end
end