Identify wrong index name
This commit is contained in:
		
							parent
							
								
									a10b2d385b
								
							
						
					
					
						commit
						8e35d8eb10
					
				@ -1,5 +1,6 @@
 | 
			
		||||
require 'verbal_expressions'
 | 
			
		||||
require 'filesize'
 | 
			
		||||
require 'cgi'
 | 
			
		||||
 | 
			
		||||
module Jekyll
 | 
			
		||||
  module Algolia
 | 
			
		||||
@ -19,12 +20,23 @@ module Jekyll
 | 
			
		||||
        exit 1
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      # Public: Will identify the error and return its internal name
 | 
			
		||||
      #
 | 
			
		||||
      # error - The caught error
 | 
			
		||||
      # context - A hash of additional information that can be passed from the
 | 
			
		||||
      # code intercepting the user
 | 
			
		||||
      #
 | 
			
		||||
      # It will parse in order all potential known issues until it finds one
 | 
			
		||||
      # that matches. Returns false if no match, or a hash of :name and :details
 | 
			
		||||
      # further identifying the issue.
 | 
			
		||||
      def self.identify(error, context = {})
 | 
			
		||||
        known_errors = %w[
 | 
			
		||||
          unknown_application_id
 | 
			
		||||
          invalid_credentials_for_tmp_index
 | 
			
		||||
          invalid_credentials
 | 
			
		||||
          record_too_big
 | 
			
		||||
          unknown_settings
 | 
			
		||||
          invalid_index_name
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        # Checking the errors against our known list
 | 
			
		||||
@ -39,6 +51,12 @@ module Jekyll
 | 
			
		||||
        false
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      # Public: Parses an Algolia error message into a hash of its content
 | 
			
		||||
      #
 | 
			
		||||
      # message - The raw message as returned by the API
 | 
			
		||||
      #
 | 
			
		||||
      # Returns a hash of all parts of the message, to be more easily consumed
 | 
			
		||||
      # by our error matchers
 | 
			
		||||
      def self.error_hash(message)
 | 
			
		||||
        message = message.delete("\n")
 | 
			
		||||
 | 
			
		||||
@ -57,9 +75,17 @@ module Jekyll
 | 
			
		||||
          find '/'
 | 
			
		||||
          capture('api_section') { word }
 | 
			
		||||
          find '/'
 | 
			
		||||
          capture('index_name') { word }
 | 
			
		||||
          capture('index_name') do
 | 
			
		||||
            anything_but('/')
 | 
			
		||||
          end
 | 
			
		||||
          find '/'
 | 
			
		||||
          capture do
 | 
			
		||||
            capture('api_action') { word }
 | 
			
		||||
            maybe '?'
 | 
			
		||||
            capture('query_parameters') do
 | 
			
		||||
              anything_but(':')
 | 
			
		||||
            end
 | 
			
		||||
          end
 | 
			
		||||
          find ': '
 | 
			
		||||
          capture('json') do
 | 
			
		||||
            find '{'
 | 
			
		||||
@ -82,10 +108,26 @@ module Jekyll
 | 
			
		||||
 | 
			
		||||
        hash['api_version'] = hash['api_version'].to_i
 | 
			
		||||
        hash['http_error'] = hash['http_error'].to_i
 | 
			
		||||
        hash['json'] = JSON.parse(hash['json'])
 | 
			
		||||
 | 
			
		||||
        # Merging the JSON key directly in the answer
 | 
			
		||||
        hash = hash.merge(JSON.parse(hash['json']))
 | 
			
		||||
        hash.delete('json')
 | 
			
		||||
        # Merging the query parameters in the answer
 | 
			
		||||
        CGI.parse(hash['query_parameters']).each do |key, values|
 | 
			
		||||
          hash[key] = values[0]
 | 
			
		||||
        end
 | 
			
		||||
        hash.delete('query_parameters')
 | 
			
		||||
 | 
			
		||||
        hash
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      # Public: Check if the application id is available
 | 
			
		||||
      #
 | 
			
		||||
      # _context - Not used
 | 
			
		||||
      #
 | 
			
		||||
      # If the call to the cluster fails, chances are that the application ID
 | 
			
		||||
      # is invalid. As we cannot actually contact the server, the error is raw
 | 
			
		||||
      # and does not follow our error spec
 | 
			
		||||
      def self.unknown_application_id?(error, _context = {})
 | 
			
		||||
        message = error.message
 | 
			
		||||
        return false if message !~ /^Cannot reach any host/
 | 
			
		||||
@ -95,6 +137,13 @@ module Jekyll
 | 
			
		||||
        { 'application_id' => matches[1] }
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      # Public: Check if credentials specifically can't access the _tmp index
 | 
			
		||||
      #
 | 
			
		||||
      # _context - Not used
 | 
			
		||||
      #
 | 
			
		||||
      # If the error happens on a _tmp folder, it might mean that the key does
 | 
			
		||||
      # not have access to the _tmp indices and the error message will reflect
 | 
			
		||||
      # that.
 | 
			
		||||
      def self.invalid_credentials_for_tmp_index?(error, _context = {})
 | 
			
		||||
        return false unless invalid_credentials?(error)
 | 
			
		||||
 | 
			
		||||
@ -109,10 +158,15 @@ module Jekyll
 | 
			
		||||
        }
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      # Public: Check if the credentials are working
 | 
			
		||||
      #
 | 
			
		||||
      # _context - Not used
 | 
			
		||||
      #
 | 
			
		||||
      # Application ID and API key submitted don't match any credentials known
 | 
			
		||||
      def self.invalid_credentials?(error, _context = {})
 | 
			
		||||
        details = error_hash(error.message)
 | 
			
		||||
 | 
			
		||||
        if details['json']['message'] != 'Invalid Application-ID or API key'
 | 
			
		||||
        if details['message'] != 'Invalid Application-ID or API key'
 | 
			
		||||
          return false
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
@ -122,16 +176,23 @@ module Jekyll
 | 
			
		||||
        }
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      # Public: Check if the sent records are not too big
 | 
			
		||||
      #
 | 
			
		||||
      # context[:records] - list of records to push
 | 
			
		||||
      #
 | 
			
		||||
      # Records cannot weight more that 10Kb. If we're getting this error it
 | 
			
		||||
      # means that one of the records is too big, so we'll try to give
 | 
			
		||||
      # informations about it so the user can debug it.
 | 
			
		||||
      def self.record_too_big?(error, context = {})
 | 
			
		||||
        details = error_hash(error.message)
 | 
			
		||||
 | 
			
		||||
        message = details['json']['message']
 | 
			
		||||
        message = details['message']
 | 
			
		||||
        return false if message !~ /^Record .* is too big .*/
 | 
			
		||||
 | 
			
		||||
        # Getting the record size
 | 
			
		||||
        size, = /.*size=(.*) bytes.*/.match(message).captures
 | 
			
		||||
        size = Filesize.from("#{size} B").pretty
 | 
			
		||||
        object_id = details['json']['objectID']
 | 
			
		||||
        object_id = details['objectID']
 | 
			
		||||
 | 
			
		||||
        # Getting record details
 | 
			
		||||
        record = Utils.find_by_key(context[:records], :objectID, object_id)
 | 
			
		||||
@ -145,6 +206,44 @@ module Jekyll
 | 
			
		||||
          'size_limit' => '10 Kb'
 | 
			
		||||
        }
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      # Public: Check if one of the index settings is invalid
 | 
			
		||||
      #
 | 
			
		||||
      # context[:settings] - The settings passed to update the index
 | 
			
		||||
      #
 | 
			
		||||
      # The API will block any call that tries to update a setting value that is
 | 
			
		||||
      # not available. We'll tell the user which one so they can fix their
 | 
			
		||||
      # issue.
 | 
			
		||||
      def self.unknown_settings?(error, context = {})
 | 
			
		||||
        details = error_hash(error.message)
 | 
			
		||||
 | 
			
		||||
        message = details['message']
 | 
			
		||||
        return false if message !~ /^Invalid object attributes.*/
 | 
			
		||||
 | 
			
		||||
        # Getting the unknown setting name
 | 
			
		||||
        regex = /^Invalid object attributes: (.*) near line.*/
 | 
			
		||||
        setting_name, = regex.match(message).captures
 | 
			
		||||
        setting_value = context[:settings][setting_name]
 | 
			
		||||
 | 
			
		||||
        {
 | 
			
		||||
          'setting_name' => setting_name,
 | 
			
		||||
          'setting_value' => setting_value
 | 
			
		||||
        }
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      # Public: Check if the index name is invalid
 | 
			
		||||
      #
 | 
			
		||||
      # Some characters are forbidden in index names
 | 
			
		||||
      def self.invalid_index_name?(error, _context = {})
 | 
			
		||||
        details = error_hash(error.message)
 | 
			
		||||
 | 
			
		||||
        message = details['message']
 | 
			
		||||
        return false if message !~ /^indexName is not valid.*/
 | 
			
		||||
 | 
			
		||||
        {
 | 
			
		||||
          'index_name' => Configurator.index_name
 | 
			
		||||
        }
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
@ -75,7 +75,12 @@ module Jekyll
 | 
			
		||||
      def self.delete_records_by_id(index, ids)
 | 
			
		||||
        Logger.log("I:Deleting #{ids.length} records")
 | 
			
		||||
        return if Configurator.dry_run?
 | 
			
		||||
 | 
			
		||||
        begin
 | 
			
		||||
          index.delete_objects!(ids)
 | 
			
		||||
        rescue StandardError => error
 | 
			
		||||
          ErrorHandler.stop(error)
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      # Public: Returns an array of all the objectIDs in the index
 | 
			
		||||
@ -108,7 +113,11 @@ module Jekyll
 | 
			
		||||
      def self.update_settings(index, settings)
 | 
			
		||||
        Logger.verbose('I:Updating settings')
 | 
			
		||||
        return if Configurator.dry_run?
 | 
			
		||||
        begin
 | 
			
		||||
          index.set_settings(settings)
 | 
			
		||||
        rescue StandardError => error
 | 
			
		||||
          ErrorHandler.stop(error, settings: settings)
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      # Public: Index content following the `diff` indexing mode
 | 
			
		||||
@ -118,10 +127,13 @@ module Jekyll
 | 
			
		||||
      # The `diff` indexing mode will only push new content to the index and
 | 
			
		||||
      # remove old content from it. It won't touch records that haven't been
 | 
			
		||||
      # updated. It will be a bit slower as it will first need to get the list
 | 
			
		||||
      # of all records in the index, but it will consume less operations than
 | 
			
		||||
      # the `atomic` indexing mode.
 | 
			
		||||
      # of all records in the index, but it will consume less operations.
 | 
			
		||||
      def self.run_diff_mode(records)
 | 
			
		||||
        index = index(Configurator.index_name)
 | 
			
		||||
 | 
			
		||||
        # Update settings
 | 
			
		||||
        update_settings(index, Configurator.settings)
 | 
			
		||||
 | 
			
		||||
        # Getting list of objectID in remote and locally
 | 
			
		||||
        remote_ids = remote_object_ids(index)
 | 
			
		||||
        local_ids = local_object_ids(records)
 | 
			
		||||
@ -135,9 +147,6 @@ module Jekyll
 | 
			
		||||
          new_records_ids.include?(record[:objectID])
 | 
			
		||||
        end
 | 
			
		||||
        update_records(index, new_records)
 | 
			
		||||
 | 
			
		||||
        # Update settings
 | 
			
		||||
        update_settings(index, Configurator.settings)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      # Public: Get the settings of the remote index
 | 
			
		||||
@ -145,6 +154,8 @@ module Jekyll
 | 
			
		||||
      # index - The Algolia Index
 | 
			
		||||
      def self.remote_settings(index)
 | 
			
		||||
        index.get_settings
 | 
			
		||||
      rescue StandardError => error
 | 
			
		||||
        ErrorHandler.stop(error)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      # Public: Rename an index
 | 
			
		||||
@ -156,7 +167,11 @@ module Jekyll
 | 
			
		||||
      def self.rename_index(old_name, new_name)
 | 
			
		||||
        Logger.verbose("I:Renaming `#{old_name}` to `#{new_name}`")
 | 
			
		||||
        return if Configurator.dry_run?
 | 
			
		||||
        begin
 | 
			
		||||
          ::Algolia.move_index(old_name, new_name)
 | 
			
		||||
        rescue StandardError => error
 | 
			
		||||
          ErrorHandler.stop(error, new_name: new_name)
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      # Public: Index content following the `atomic` indexing mode
 | 
			
		||||
@ -177,14 +192,14 @@ module Jekyll
 | 
			
		||||
 | 
			
		||||
        Logger.verbose("I:Using `#{index_tmp_name}` as temporary index")
 | 
			
		||||
 | 
			
		||||
        # Pushing everthing to a brand new index
 | 
			
		||||
        update_records(index_tmp, records)
 | 
			
		||||
 | 
			
		||||
        # Copying original settings to the new index
 | 
			
		||||
        remote_settings = remote_settings(index)
 | 
			
		||||
        new_settings = remote_settings.merge(Configurator.settings)
 | 
			
		||||
        update_settings(index_tmp, new_settings)
 | 
			
		||||
 | 
			
		||||
        # Pushing everthing to a brand new index
 | 
			
		||||
        update_records(index_tmp, records)
 | 
			
		||||
 | 
			
		||||
        # Renaming the new index in place of the old
 | 
			
		||||
        rename_index(index_tmp_name, index_name)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,63 @@ describe(Jekyll::Algolia::ErrorHandler) do
 | 
			
		||||
  let(:current) { Jekyll::Algolia::ErrorHandler }
 | 
			
		||||
  let(:configurator) { Jekyll::Algolia::Configurator }
 | 
			
		||||
 | 
			
		||||
  describe '.error_hash' do
 | 
			
		||||
    subject { current.error_hash(message) }
 | 
			
		||||
 | 
			
		||||
    context 'with a regular error message' do
 | 
			
		||||
      let(:message) do
 | 
			
		||||
        'Cannot POST to '\
 | 
			
		||||
        'https://MY_APP_ID.algolia.net/1/section/index_name/action: '\
 | 
			
		||||
        '{"message":"Custom message","status":403}'\
 | 
			
		||||
        "\n (403)"
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it do
 | 
			
		||||
        should include('verb' => 'POST')
 | 
			
		||||
        should include('scheme' => 'https')
 | 
			
		||||
        should include('application_id' => 'MY_APP_ID')
 | 
			
		||||
        should include('api_version' => 1)
 | 
			
		||||
        should include('api_section' => 'section')
 | 
			
		||||
        should include('index_name' => 'index_name')
 | 
			
		||||
        should include('api_action' => 'action')
 | 
			
		||||
        should include('message' => 'Custom message')
 | 
			
		||||
        should include('status' => 403)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'with a message with query parameters' do
 | 
			
		||||
      let(:message) do
 | 
			
		||||
        'Cannot POST to '\
 | 
			
		||||
        'https://MY_APP_ID.algolia.net/1/section/index_name/action?foo=bar: '\
 | 
			
		||||
        '{"message":"Custom message","status":403}'\
 | 
			
		||||
        "\n (403)"
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it do
 | 
			
		||||
        should include('foo' => 'bar')
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'with an error message with weird characaters' do
 | 
			
		||||
      let(:message) do
 | 
			
		||||
        'Cannot POST to '\
 | 
			
		||||
        'https://MY_APP_ID.algolia.net/1/section/index_name$`!</action: '\
 | 
			
		||||
        '{"message":"Custom message","status":403}'\
 | 
			
		||||
        "\n (403)"
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it do
 | 
			
		||||
        should include('index_name' => 'index_name$`!<')
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'with a malformed error message' do
 | 
			
		||||
      let(:message) { 'Unable to even parse this' }
 | 
			
		||||
 | 
			
		||||
      it { should eq false }
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '.identify' do
 | 
			
		||||
    subject { current.identify(error, context) }
 | 
			
		||||
 | 
			
		||||
@ -72,7 +129,7 @@ describe(Jekyll::Algolia::ErrorHandler) do
 | 
			
		||||
    context 'with a record too big' do
 | 
			
		||||
      let(:message) do
 | 
			
		||||
        '400: Cannot POST to '\
 | 
			
		||||
        'https://MXM0JWJNIW.algolia.net/1/indexes/my_index/batch: '\
 | 
			
		||||
        'https://MY_APP_ID.algolia.net/1/indexes/my_index/batch: '\
 | 
			
		||||
        '{"message":"Record at the position 3 '\
 | 
			
		||||
        'objectID=deadbeef is too big size=1091966 bytes. '\
 | 
			
		||||
        'Contact us if you need an extended quota","position":3,'\
 | 
			
		||||
@ -102,5 +159,47 @@ describe(Jekyll::Algolia::ErrorHandler) do
 | 
			
		||||
        expect(details).to include('size_limit' => '10 Kb')
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'with an unknown setting' do
 | 
			
		||||
      let(:message) do
 | 
			
		||||
        '400: Cannot PUT to '\
 | 
			
		||||
        'https://MY_APP_ID.algolia.net/1/indexes/my_index/settings: '\
 | 
			
		||||
        '{"message":"Invalid object attributes: deadbeef near line:1 column:456",'\
 | 
			
		||||
        '"status":400} (400)'
 | 
			
		||||
      end
 | 
			
		||||
      let(:context) do
 | 
			
		||||
        { settings:
 | 
			
		||||
          {
 | 
			
		||||
            'searchableAttributes' => %w[foo bar],
 | 
			
		||||
            'deadbeef' => 'foofoo'
 | 
			
		||||
          } }
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it { should include(name: 'unknown_settings') }
 | 
			
		||||
      it do
 | 
			
		||||
        details = subject[:details]
 | 
			
		||||
        expect(details).to include('setting_name' => 'deadbeef')
 | 
			
		||||
        expect(details).to include('setting_value' => 'foofoo')
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'with an invalid index name' do
 | 
			
		||||
      before do
 | 
			
		||||
        allow(configurator)
 | 
			
		||||
          .to receive(:index_name)
 | 
			
		||||
          .and_return('invalid_index_name')
 | 
			
		||||
      end
 | 
			
		||||
      let(:message) do
 | 
			
		||||
        '400: Cannot GET to '\
 | 
			
		||||
        'https://MY_APP_ID-dsn.algolia.net/1/indexes/invalid_index_name/settings?getVersion=2: '\
 | 
			
		||||
        '{"message":"indexName is not valid","status":400} (400)'
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it { should include(name: 'invalid_index_name') }
 | 
			
		||||
      it do
 | 
			
		||||
        details = subject[:details]
 | 
			
		||||
        expect(details).to include('index_name' => 'invalid_index_name')
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user