require 'spec_helper'

describe 'Project mirror', :js do
  include ReactiveCachingHelpers

  let(:project) { create(:project, :repository, creator: user, name: 'Victorialand') }
  let(:import_state) { create(:import_state, :mirror, :finished, project: project) }
  let(:user) { create(:user) }

  describe 'On a project' do
    before do
      project.add_maintainer(user)
      sign_in user
    end

    context 'with Update now button' do
      let(:timestamp) { Time.now }

      before do
        import_state.update(next_execution_timestamp: timestamp + 10.minutes)
      end

      context 'when able to force update' do
        it 'forces import' do
          import_state.update(last_update_at: timestamp - 8.minutes)

          expect_any_instance_of(EE::ProjectImportState).to receive(:force_import_job!)

          Timecop.freeze(timestamp) do
            visit project_mirror_path(project)
          end

          Sidekiq::Testing.fake! { find('.js-force-update-mirror').click }
        end
      end

      context 'when unable to force update' do
        it 'does not force import' do
          import_state.update(last_update_at: timestamp - 3.minutes)

          expect_any_instance_of(EE::ProjectImportState).not_to receive(:force_import_job!)

          Timecop.freeze(timestamp) do
            visit project_mirror_path(project)
          end

          expect(page).to have_selector('.js-force-update-mirror')
          expect(page).to have_selector('.btn.disabled')
        end
      end
    end
  end

  describe 'configuration' do
    # Start from a project with no mirroring set up
    let(:project) { create(:project, :repository, creator: user) }
    let(:import_data) { project.reload_import_data }

    before do
      project.add_maintainer(user)
      sign_in(user)
    end

    describe 'password authentication' do
      it 'can be set up' do
        visit project_settings_repository_path(project)

        page.within('.project-mirror-settings') do
          fill_in 'Git repository URL', with: 'http://user@example.com'
          select 'Pull', from: 'Mirror direction'
          fill_in 'Password', with: 'foo'
          click_without_sidekiq 'Mirror repository'
        end

        expect(page).to have_content('Mirroring settings were successfully updated')

        project.reload
        expect(project.mirror?).to be_truthy
        expect(import_data.auth_method).to eq('password')
        expect(project.import_url).to eq('http://user:foo@example.com')
      end

      it 'can be changed to unauthenticated' do
        project.update!(import_url: 'http://user:password@example.com')

        visit project_settings_repository_path(project)

        page.within('.project-mirror-settings') do
          fill_in 'Git repository URL', with: 'http://2.example.com'
          select('Pull', from: 'Mirror direction')
          fill_in 'Password', with: ''
          click_without_sidekiq 'Mirror repository'
        end

        expect(page).to have_content('Mirroring settings were successfully updated')

        project.reload
        expect(import_data.auth_method).to eq('password')
        expect(project.import_url).to eq('http://2.example.com')
      end

      it 'can be recreated after an SSH mirror is set' do
        visit project_settings_repository_path(project)

        page.within('.project-mirror-settings') do
          fill_in 'Git repository URL', with: 'ssh://user@example.com'
          select('Pull', from: 'Mirror direction')
          select 'SSH public key', from: 'Authentication method'

          # Generates an SSH public key with an asynchronous PUT and displays it
          wait_for_requests

          click_without_sidekiq 'Mirror repository'
        end

        expect(page).to have_content('Mirroring settings were successfully updated')

        find('.js-delete-pull-mirror').click

        page.within('.project-mirror-settings') do
          fill_in 'Git repository URL', with: 'http://git@example.com'
          select('Pull', from: 'Mirror direction')
          fill_in 'Password', with: 'test_password'
          click_without_sidekiq 'Mirror repository'
        end

        expect(page).to have_content('Mirroring settings were successfully updated')

        project.reload
        expect(import_data.auth_method).to eq('password')
        expect(import_data.password).to eq('test_password')
        expect(project.import_url).to eq('http://git:test_password@example.com')
      end
    end

    describe 'SSH public key authentication' do
      it 'can be set up' do
        visit project_settings_repository_path(project)

        page.within('.project-mirror-settings') do
          fill_in 'Git repository URL', with: 'ssh://user@example.com'
          select('Pull', from: 'Mirror direction')
          select 'SSH public key', from: 'Authentication method'

          # Generates an SSH public key with an asynchronous PUT and displays it
          wait_for_requests

          expect(import_data.ssh_public_key).not_to be_nil
          expect(page).to have_content(import_data.ssh_public_key)

          click_without_sidekiq 'Mirror repository'
        end

        # We didn't set any host keys
        expect(page).to have_content('Mirroring settings were successfully updated')
        expect(page).not_to have_content('Verified by')

        project.reload
        import_data.reload
        expect(project.mirror?).to be_truthy
        expect(project.username_only_import_url).to eq('ssh://user@example.com')
        expect(import_data.auth_method).to eq('ssh_public_key')
        expect(import_data.password).to be_blank

        find('.js-delete-mirror').click
        fill_in 'Git repository URL', with: 'ssh://user@example.com'
        select('Pull', from: 'Mirror direction')

        first_key = import_data.ssh_public_key
        expect(page).to have_content(first_key)

        # Check regenerating the public key works
        click_without_sidekiq 'Regenerate key'
        find('.js-regenerate-public-ssh-key-confirm-modal .js-confirm').click

        wait_for_requests

        expect(page).not_to have_content(first_key)
        expect(page).to have_content(import_data.reload.ssh_public_key)
      end
    end

    describe 'host key management', :use_clean_rails_memory_store_caching do
      let(:key) { Gitlab::SSHPublicKey.new(SSHKeygen.generate) }
      let(:cache) { SshHostKey.new(project: project, url: "ssh://example.com:22") }

      it 'fills fingerprints and host keys when detecting' do
        stub_reactive_cache(cache, known_hosts: key.key_text)

        visit project_settings_repository_path(project)

        page.within('.project-mirror-settings') do
          fill_in 'Git repository URL', with: 'ssh://example.com'
          select('Pull', from: 'Mirror direction')
          click_on 'Detect host keys'

          wait_for_requests

          expect(page).to have_content(key.fingerprint)

          click_on 'Input host keys manually'

          expect(page).to have_field('SSH host keys', with: key.key_text)
        end
      end

      it 'preserves the existing SSH key after generating it once' do
        stub_reactive_cache(cache, known_hosts: key.key_text)

        visit project_settings_repository_path(project)

        page.within('.project-mirror-settings') do
          fill_in 'Git repository URL', with: 'ssh://example.com'
          select('Pull', from: 'Mirror direction')
          select 'SSH public key', from: 'Authentication method'
          click_on 'Detect host keys'

          wait_for_requests

          expect(page).to have_content(key.fingerprint)

          wait_for_requests

          expect { click_on 'Mirror repository' }.not_to change { import_data.reload.ssh_public_key }
        end
      end

      it 'displays error if detection fails' do
        stub_reactive_cache(cache, error: 'Some error text here')

        visit project_settings_repository_path(project)

        page.within('.project-mirror-settings') do
          fill_in 'Git repository URL', with: 'ssh://example.com'
          select('Pull', from: 'Mirror direction')
          click_on 'Detect host keys'

          wait_for_requests
        end

        # Appears in the flash
        expect(page).to have_content('Some error text here')
      end

      it 'allows manual host keys entry' do
        visit project_settings_repository_path(project)

        page.within('.project-mirror-settings') do
          fill_in 'Git repository URL', with: 'ssh://example.com'
          select('Pull', from: 'Mirror direction')
          click_on 'Input host keys manually'
          fill_in 'SSH host keys', with: "example.com #{key.key_text}"
          click_without_sidekiq 'Mirror repository'

          find('.js-delete-mirror').click
          fill_in 'Git repository URL', with: 'ssh://example.com'
          select('Pull', from: 'Mirror direction')

          expect(page).to have_content(key.fingerprint)
          expect(page).to have_content("Verified by #{h(user.name)} less than a minute ago")
        end
      end
    end

    describe 'authentication methods' do
      it 'shows SSH related fields for an SSH URL' do
        visit project_settings_repository_path(project)

        page.within('.project-mirror-settings') do
          fill_in 'Git repository URL', with: 'ssh://example.com'
          select('Pull', from: 'Mirror direction')

          execute_script 'document.querySelector("html").scrollTop = 1000;'
          expect(page).to have_select('Authentication method')

          # SSH can use password authentication but needs host keys
          select 'Password', from: 'Authentication method'
          expect(page).to have_field('Password')
          expect(page).to have_button('Detect host keys')
          expect(page).to have_button('Input host keys manually')

          # SSH public key authentication also needs host keys but no password
          select 'SSH public key', from: 'Authentication method'
          expect(page).not_to have_field('Password')
          expect(page).to have_button('Detect host keys')
          expect(page).to have_button('Input host keys manually')
        end
      end

      it 'hides SSH-related fields for a HTTP URL' do
        visit project_settings_repository_path(project)

        page.within('.project-mirror-settings') do
          fill_in 'Git repository URL', with: 'https://example.com'
          select('Pull', from: 'Mirror direction')

          # HTTPS can't use public key authentication and doesn't need host keys
          expect(page).to have_field('Password')
          expect(page).not_to have_select('Authentication method')
          expect(page).not_to have_button('Detect host keys')
          expect(page).not_to have_button('Input host keys manually')
        end
      end
    end

    def click_without_sidekiq(*args)
      Sidekiq::Testing.fake! { click_on(*args) }
    end
  end
end
