# frozen_string_literal: true

require 'spec_helper'

describe Mongo::Auth::User::View do
  let(:database) { root_authorized_client.database }

  let(:view) do
    described_class.new(database)
  end

  before do
    # Separate view instance to not interfere with test assertions
    view = described_class.new(root_authorized_client.database)
    begin
      view.remove('durran')
    rescue Mongo::Error::OperationFailure
    end
  end

  shared_context 'testing write concern' do
    let(:subscriber) do
      Mrss::EventSubscriber.new
    end

    let(:client) do
      root_authorized_client.tap do |client|
        client.subscribe(Mongo::Monitoring::COMMAND, subscriber)
      end
    end

    let(:view) do
      described_class.new(client.database)
    end

    before do
      allow_any_instance_of(Mongo::Monitoring::Event::CommandStarted).to receive(:redacted) do |_instance, _command_name, document|
        document
      end
    end
  end

  shared_examples_for 'forwards write concern to server' do
    # w:2 requires more than one node in the deployment
    require_topology :replica_set

    it 'forwards write concern to server' do
      response

      expect(event.command['writeConcern']).to eq('w' => 2)
    end
  end

  describe '#create' do
    context 'when password is not provided' do
      let(:database) { root_authorized_client.use('$external').database }

      let(:username) { 'passwordless-user' }

      let(:response) do
        view.create(
          username,
          # https://stackoverflow.com/questions/55939832/mongodb-external-database-cannot-create-new-user-with-user-defined-role
          roles: [ { role: 'read', db: 'admin' } ]
        )
      end

      before do
        view.remove(username)
      rescue Mongo::Error::OperationFailure
        # can be user not found, ignore
      end

      it 'creates the user' do
        expect(view.info(username)).to eq([])

        lambda do
          response
        end.should_not raise_error

        expect(view.info(username).first['user']).to eq(username)
      end
    end

    context 'when a session is not used' do
      let!(:response) do
        view.create(
          'durran',
          {
            password: 'password',
            roles: [ Mongo::Auth::Roles::READ_WRITE ],
          }
        )
      end

      context 'when user creation was successful' do
        it 'saves the user in the database' do
          expect(response).to be_successful
        end

        context 'when compression is used' do
          require_compression

          it 'does not compress the message' do
            expect(Mongo::Protocol::Compressed).not_to receive(:new)
            expect(response).to be_successful
          end
        end
      end

      context 'when creation was not successful' do
        it 'raises an exception' do
          expect do
            view.create('durran', password: 'password')
          end.to raise_error(Mongo::Error::OperationFailure)
        end
      end
    end

    context 'when a session is used' do
      let(:operation) do
        view.create(
          'durran',
          password: 'password',
          roles: [ Mongo::Auth::Roles::READ_WRITE ],
          session: session
        )
      end

      let(:session) do
        client.start_session
      end

      let(:client) do
        root_authorized_client
      end

      it_behaves_like 'an operation using a session'
    end

    context 'when write concern is given' do
      include_context 'testing write concern'

      let(:response) do
        view.create(
          'durran',
          password: 'password',
          roles: [ Mongo::Auth::Roles::READ_WRITE ],
          write_concern: { w: 2 }
        )
      end

      let(:event) do
        subscriber.single_command_started_event('createUser')
      end

      it_behaves_like 'forwards write concern to server'
    end
  end

  describe '#update' do
    before do
      view.create(
        'durran',
        password: 'password', roles: [ Mongo::Auth::Roles::READ_WRITE ]
      )
    end

    context 'when a user password is updated' do
      context 'when a session is not used' do
        let!(:response) do
          view.update(
            'durran',
            password: '123', roles: [ Mongo::Auth::Roles::READ_WRITE ]
          )
        end

        it 'updates the password' do
          expect(response).to be_successful
        end

        context 'when compression is used' do
          require_compression

          it 'does not compress the message' do
            expect(Mongo::Protocol::Compressed).not_to receive(:new)
            expect(response).to be_successful
          end
        end
      end

      context 'when a session is used' do
        let(:operation) do
          view.update(
            'durran',
            password: '123',
            roles: [ Mongo::Auth::Roles::READ_WRITE ],
            session: session
          )
        end

        let(:session) do
          client.start_session
        end

        let(:client) do
          root_authorized_client
        end

        it_behaves_like 'an operation using a session'
      end
    end

    context 'when the roles of a user are updated' do
      context 'when a session is not used' do
        let!(:response) do
          view.update(
            'durran',
            password: 'password', roles: [ Mongo::Auth::Roles::READ ]
          )
        end

        it 'updates the roles' do
          expect(response).to be_successful
        end

        context 'when compression is used' do
          require_compression

          it 'does not compress the message' do
            expect(Mongo::Protocol::Compressed).not_to receive(:new)
            expect(response).to be_successful
          end
        end
      end

      context 'when a session is used' do
        let(:operation) do
          view.update(
            'durran',
            password: 'password',
            roles: [ Mongo::Auth::Roles::READ ],
            session: session
          )
        end

        let(:session) do
          client.start_session
        end

        let(:client) do
          root_authorized_client
        end

        it_behaves_like 'an operation using a session'
      end
    end

    context 'when write concern is given' do
      include_context 'testing write concern'

      let(:response) do
        view.update(
          'durran',
          password: 'password1',
          roles: [ Mongo::Auth::Roles::READ_WRITE ],
          write_concern: { w: 2 }
        )
      end

      let(:event) do
        subscriber.single_command_started_event('updateUser')
      end

      it_behaves_like 'forwards write concern to server'
    end
  end

  describe '#remove' do
    context 'when a session is not used' do
      context 'when user removal was successful' do
        before do
          view.create(
            'durran',
            password: 'password', roles: [ Mongo::Auth::Roles::READ_WRITE ]
          )
        end

        let(:response) do
          view.remove('durran')
        end

        it 'saves the user in the database' do
          expect(response).to be_successful
        end
      end

      context 'when removal was not successful' do
        it 'raises an exception' do
          expect do
            view.remove('notauser')
          end.to raise_error(Mongo::Error::OperationFailure)
        end
      end
    end

    context 'when a session is used' do
      context 'when user removal was successful' do
        before do
          view.create(
            'durran',
            password: 'password', roles: [ Mongo::Auth::Roles::READ_WRITE ]
          )
        end

        let(:operation) do
          view.remove('durran', session: session)
        end

        let(:session) do
          client.start_session
        end

        let(:client) do
          root_authorized_client
        end

        it_behaves_like 'an operation using a session'
      end

      context 'when removal was not successful' do
        let(:failed_operation) do
          view.remove('notauser', session: session)
        end

        let(:session) do
          client.start_session
        end

        let(:client) do
          root_authorized_client
        end

        it_behaves_like 'a failed operation using a session'
      end
    end

    context 'when write concern is given' do
      include_context 'testing write concern'

      before do
        view.create(
          'durran',
          password: 'password', roles: [ Mongo::Auth::Roles::READ_WRITE ]
        )
      end

      let(:response) do
        view.remove(
          'durran',
          write_concern: { w: 2 }
        )
      end

      let(:event) do
        subscriber.single_command_started_event('dropUser')
      end

      it_behaves_like 'forwards write concern to server'
    end
  end

  describe '#info' do
    context 'when a session is not used' do
      before do
        view.remove('emily')
      rescue StandardError
        nil
      end

      context 'when a user exists in the database' do
        before do
          view.create(
            'emily',
            password: 'password'
          )
        end

        it 'returns information for that user' do
          expect(view.info('emily')).not_to be_empty
        end
      end

      context 'when a user does not exist in the database' do
        it 'returns nil' do
          expect(view.info('emily')).to be_empty
        end
      end

      context 'when a user is not authorized' do
        require_auth

        let(:view) do
          described_class.new(unauthorized_client.database)
        end

        it 'raises an OperationFailure' do
          expect do
            view.info('emily')
          end.to raise_exception(Mongo::Error::OperationFailure)
        end
      end
    end

    context 'when a session is used' do
      context 'when a user exists in the database' do
        before do
          view.create(
            'durran',
            password: 'password'
          )
        end

        let(:operation) do
          view.info('durran', session: session)
        end

        let(:session) do
          client.start_session
        end

        let(:client) do
          root_authorized_client
        end

        it_behaves_like 'an operation using a session'
      end

      context 'when a user does not exist in the database' do
        let(:operation) do
          view.info('emily', session: session)
        end

        let(:session) do
          client.start_session
        end

        let(:client) do
          root_authorized_client
        end

        it_behaves_like 'an operation using a session'
      end
    end
  end

  context 'when the result is a write concern error' do
    require_topology :replica_set

    let(:user) do
      Mongo::Auth::User.new({
                              user: 'user',
                              roles: [ Mongo::Auth::Roles::READ_WRITE ],
                              password: 'password'
                            })
    end

    before do
      authorized_client.use('admin').database.command(
        configureFailPoint: 'failCommand',
        mode: { times: 1 },
        data: {
          failCommands: [ failCommand ],
          writeConcernError: {
            code: 64,
            codeName: 'WriteConcernTimeout',
            errmsg: 'waiting for replication timed out',
            errInfo: { wtimeout: true }
          }
        }
      )
    end

    shared_examples 'raises the correct write concern error' do
      it 'raises a write concern error' do
        expect do
          view.send(method, input)
        end.to raise_error(Mongo::Error::OperationFailure, /[64:WriteConcFald]/)
      end

      it 'raises and reports the write concern error correctly' do
        view.send(method, input)
      rescue Mongo::Error::OperationFailure::Family => e
        expect(e.write_concern_error?).to be true
        expect(e.write_concern_error_document).to eq(
          'code' => 64,
          'codeName' => 'WriteConcernTimeout',
          'errmsg' => 'waiting for replication timed out',
          'errInfo' => { 'wtimeout' => true }
        )
      end
    end

    context 'when creating a user' do
      let(:failCommand) { 'createUser' }
      let(:method) { :create }
      let(:input) { user }

      after do
        view.remove(user.name)
      end

      include_examples 'raises the correct write concern error'
    end

    context 'when updating a user' do
      let(:failCommand) { 'updateUser' }
      let(:method) { :update }
      let(:input) { user.name }

      before do
        view.create(user)
      end

      after do
        view.remove(user.name)
      end

      include_examples 'raises the correct write concern error'
    end

    context 'when removing a user' do
      let(:failCommand) { 'dropUser' }
      let(:method) { :remove }
      let(:input) { user.name }

      before do
        view.create(user)
      end

      include_examples 'raises the correct write concern error'
    end
  end
end
