# frozen_string_literal: true
# rubocop:todo all

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
        begin
          view.remove(username)
        rescue Mongo::Error::OperationFailure
          # can be user not found, ignore
        end
      end

      it 'creates the user' do
        view.info(username).should == []

        lambda do
          response
        end.should_not raise_error

        view.info(username).first['user'].should == 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
          min_server_fcv '3.6'

          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 {
            view.create('durran', password: 'password')
          }.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
          min_server_fcv '3.6'

          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
          min_server_fcv '3.6'

          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 {
            view.remove('notauser')
          }.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 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')).to_not 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
    min_server_version '4.0'

    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: "WriteConcernFailed",
              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:WriteConcernFailed]/)
      end

      it "raises and reports the write concern error correctly" do
        begin
          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" => "WriteConcernFailed",
            "errmsg" => "waiting for replication timed out",
            "errInfo" => { "wtimeout" => true }
          )
        end
      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
