require 'spec_helper'

feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: true, js: true do
  include WaitForAjax

  before { allow_any_instance_of(U2fHelper).to receive(:inject_u2f_api?).and_return(true) }

  def manage_two_factor_authentication
    click_on 'Manage Two-Factor Authentication'
    expect(page).to have_content("Setup New U2F Device")
    wait_for_ajax
  end

  def register_u2f_device(u2f_device = nil)
    name = FFaker::Name.first_name
    u2f_device ||= FakeU2fDevice.new(page, name)
    u2f_device.respond_to_u2f_registration
    click_on 'Setup New U2F Device'
    expect(page).to have_content('Your device was successfully set up')
    fill_in "Pick a name", with: name
    click_on 'Register U2F Device'
    u2f_device
  end

  describe "registration" do
    let(:user) { create(:user) }

    before do
      login_as(user)
      user.update_attribute(:otp_required_for_login, true)
    end

    describe 'when 2FA via OTP is disabled' do
      before { user.update_attribute(:otp_required_for_login, false) }

      it 'does not allow registering a new device' do
        visit profile_account_path
        click_on 'Enable Two-Factor Authentication'

        expect(page).to have_button('Setup New U2F Device', disabled: true)
      end
    end

    describe 'when 2FA via OTP is enabled' do
      it 'allows registering a new device with a name' do
        visit profile_account_path
        manage_two_factor_authentication
        expect(page).to have_content("You've already enabled two-factor authentication using mobile")

        u2f_device = register_u2f_device

        expect(page).to have_content(u2f_device.name)
        expect(page).to have_content('Your U2F device was registered')
      end

      it 'allows registering more than one device' do
        visit profile_account_path

        # First device
        manage_two_factor_authentication
        first_device = register_u2f_device
        expect(page).to have_content('Your U2F device was registered')

        # Second device
        second_device = register_u2f_device
        expect(page).to have_content('Your U2F device was registered')

        expect(page).to have_content(first_device.name)
        expect(page).to have_content(second_device.name)
        expect(U2fRegistration.count).to eq(2)
      end

      it 'allows deleting a device' do
        visit profile_account_path
        manage_two_factor_authentication
        expect(page).to have_content("You've already enabled two-factor authentication using mobile")

        first_u2f_device = register_u2f_device
        second_u2f_device = register_u2f_device

        click_on "Delete", match: :first

        expect(page).to have_content('Successfully deleted')
        expect(page.body).not_to match(first_u2f_device.name)
        expect(page).to have_content(second_u2f_device.name)
      end
    end

    it 'allows the same device to be registered for multiple users' do
      # First user
      visit profile_account_path
      manage_two_factor_authentication
      u2f_device = register_u2f_device
      expect(page).to have_content('Your U2F device was registered')
      logout

      # Second user
      user = login_as(:user)
      user.update_attribute(:otp_required_for_login, true)
      visit profile_account_path
      manage_two_factor_authentication
      register_u2f_device(u2f_device)
      expect(page).to have_content('Your U2F device was registered')

      expect(U2fRegistration.count).to eq(2)
    end

    context "when there are form errors" do
      it "doesn't register the device if there are errors" do
        visit profile_account_path
        manage_two_factor_authentication

        # Have the "u2f device" respond with bad data
        page.execute_script("u2f.register = function(_,_,_,callback) { callback('bad response'); };")
        click_on 'Setup New U2F Device'
        expect(page).to have_content('Your device was successfully set up')
        click_on 'Register U2F Device'

        expect(U2fRegistration.count).to eq(0)
        expect(page).to have_content("The form contains the following error")
        expect(page).to have_content("did not send a valid JSON response")
      end

      it "allows retrying registration" do
        visit profile_account_path
        manage_two_factor_authentication

        # Failed registration
        page.execute_script("u2f.register = function(_,_,_,callback) { callback('bad response'); };")
        click_on 'Setup New U2F Device'
        expect(page).to have_content('Your device was successfully set up')
        click_on 'Register U2F Device'
        expect(page).to have_content("The form contains the following error")

        # Successful registration
        register_u2f_device

        expect(page).to have_content('Your U2F device was registered')
        expect(U2fRegistration.count).to eq(1)
      end
    end
  end

  describe "authentication" do
    let(:user) { create(:user) }

    before do
      # Register and logout
      login_as(user)
      user.update_attribute(:otp_required_for_login, true)
      visit profile_account_path
      manage_two_factor_authentication
      @u2f_device = register_u2f_device
      logout
    end

    describe "when 2FA via OTP is disabled" do
      it "allows logging in with the U2F device" do
        user.update_attribute(:otp_required_for_login, false)
        login_with(user)

        @u2f_device.respond_to_u2f_authentication

        expect(page).to have_content('We heard back from your U2F device')
        expect(page).to have_css('.sign-out-link', visible: false)
      end
    end

    describe "when 2FA via OTP is enabled" do
      it "allows logging in with the U2F device" do
        user.update_attribute(:otp_required_for_login, true)
        login_with(user)

        @u2f_device.respond_to_u2f_authentication

        expect(page).to have_content('We heard back from your U2F device')
        expect(page).to have_css('.sign-out-link', visible: false)
      end
    end

    it 'persists remember_me value via hidden field' do
      login_with(user, remember: true)

      @u2f_device.respond_to_u2f_authentication
      expect(page).to have_content('We heard back from your U2F device')

      within 'div#js-authenticate-u2f' do
        field = first('input#user_remember_me', visible: false)
        expect(field.value).to eq '1'
      end
    end

    describe "when a given U2F device has already been registered by another user" do
      describe "but not the current user" do
        it "does not allow logging in with that particular device" do
          # Register current user with the different U2F device
          current_user = login_as(:user)
          current_user.update_attribute(:otp_required_for_login, true)
          visit profile_account_path
          manage_two_factor_authentication
          register_u2f_device
          logout

          # Try authenticating user with the old U2F device
          login_as(current_user)
          @u2f_device.respond_to_u2f_authentication
          expect(page).to have_content('We heard back from your U2F device')
          expect(page).to have_content('Authentication via U2F device failed')
        end
      end

      describe "and also the current user" do
        it "allows logging in with that particular device" do
          # Register current user with the same U2F device
          current_user = login_as(:user)
          current_user.update_attribute(:otp_required_for_login, true)
          visit profile_account_path
          manage_two_factor_authentication
          register_u2f_device(@u2f_device)
          logout

          # Try authenticating user with the same U2F device
          login_as(current_user)
          @u2f_device.respond_to_u2f_authentication
          expect(page).to have_content('We heard back from your U2F device')

          expect(page).to have_css('.sign-out-link', visible: false)
        end
      end
    end

    describe "when a given U2F device has not been registered" do
      it "does not allow logging in with that particular device" do
        unregistered_device = FakeU2fDevice.new(page, FFaker::Name.first_name)
        login_as(user)
        unregistered_device.respond_to_u2f_authentication
        expect(page).to have_content('We heard back from your U2F device')

        expect(page).to have_content('Authentication via U2F device failed')
      end
    end

    describe "when more than one device has been registered by the same user" do
      it "allows logging in with either device" do
        # Register first device
        user = login_as(:user)
        user.update_attribute(:otp_required_for_login, true)
        visit profile_two_factor_auth_path
        expect(page).to have_content("Your U2F device needs to be set up.")
        first_device = register_u2f_device

        # Register second device
        visit profile_two_factor_auth_path
        expect(page).to have_content("Your U2F device needs to be set up.")
        second_device = register_u2f_device
        logout

        # Authenticate as both devices
        [first_device, second_device].each do |device|
          login_as(user)
          device.respond_to_u2f_authentication
          expect(page).to have_content('We heard back from your U2F device')

          expect(page).to have_css('.sign-out-link', visible: false)

          logout
        end
      end
    end

    describe "when two-factor authentication is disabled" do
      let(:user) { create(:user) }

      before do
        user = login_as(:user)
        user.update_attribute(:otp_required_for_login, true)
        visit profile_account_path
        manage_two_factor_authentication
        expect(page).to have_content("Your U2F device needs to be set up.")
        register_u2f_device
      end

      it "deletes u2f registrations" do
        visit profile_account_path
        expect { click_on "Disable" }.to change { U2fRegistration.count }.by(-1)
      end
    end
  end

  describe 'fallback code authentication' do
    let(:user) { create(:user) }

    def assert_fallback_ui(page)
      expect(page).to have_button('Verify code')
      expect(page).to have_css('#user_otp_attempt')
      expect(page).not_to have_link('Sign in via 2FA code')
      expect(page).not_to have_css('#js-authenticate-u2f')
    end

    before do
      # Register and logout
      login_as(user)
      user.update_attribute(:otp_required_for_login, true)
      visit profile_account_path
    end

    describe 'when no u2f device is registered' do
      before do
        logout
        login_with(user)
      end

      it 'shows the fallback otp code UI' do
        assert_fallback_ui(page)
      end
    end

    describe 'when a u2f device is registered' do
      before do
        manage_two_factor_authentication
        @u2f_device = register_u2f_device
        logout
        login_with(user)
      end

      it 'provides a button that shows the fallback otp code UI' do
        expect(page).to have_link('Sign in via 2FA code')

        click_link('Sign in via 2FA code')

        assert_fallback_ui(page)
      end
    end
  end
end
