2019-10-21 11:53:42 +02:00

941 lines
25 KiB
Ruby
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# frozen_string_literal: true
require 'spec_helper'
# rubocop:disable Metrics/BlockLength
describe(Jekyll::Algolia::Indexer) do
let(:current) { Jekyll::Algolia::Indexer }
let(:configurator) { Jekyll::Algolia::Configurator }
let(:logger) { Jekyll::Algolia::Logger }
let(:error_handler) { Jekyll::Algolia::ErrorHandler }
let(:progress_bar) { Jekyll::Algolia::ProgressBar }
let(:utils) { Jekyll::Algolia::Utils }
let(:html_extractor) { AlgoliaHTMLExtractor }
let(:dry_run) { false }
before { allow(configurator).to receive(:dry_run?).and_return(dry_run) }
before { allow(logger).to receive(:log) }
describe '.init' do
before do
allow(configurator).to receive(:application_id).and_return('app_id')
allow(configurator).to receive(:api_key).and_return('api_key')
allow(::Algolia).to receive(:init)
allow(::Algolia::Index).to receive(:new)
allow(current).to receive(:set_user_agent)
end
describe 'should instanciate Algolia with application id and api_key' do
before { current.init }
it do
expect(::Algolia)
.to have_received(:init)
.with(hash_including(
application_id: 'app_id',
api_key: 'api_key'
))
end
end
describe 'should set the user agent' do
before { current.init }
it do
expect(current).to have_received(:set_user_agent)
end
end
describe 'should make the index accessible' do
let(:index) { double('Algolia::Index') }
before do
allow(configurator)
.to receive(:index_name)
.and_return('index_name')
allow(::Algolia::Index)
.to receive(:new)
.with('index_name')
.and_return(index)
current.init
end
it do
expect(current.index).to eq index
end
end
describe 'should make the index for object ids accessible' do
let(:index_object_ids) { double('Algolia::Index') }
before do
allow(configurator)
.to receive(:index_object_ids_name)
.and_return('foo')
allow(::Algolia::Index)
.to receive(:new)
.with('foo')
.and_return(index_object_ids)
current.init
end
it do
expect(current.index_object_ids).to eq index_object_ids
end
end
end
describe '.set_user_agent' do
let(:user_agent) do
'Jekyll Integration (vIntegration); '\
'Algolia for Ruby (vAlgolia); '\
'Jekyll (vJekyll); '\
'Ruby (vRuby)'
end
before do
stub_const('Jekyll::Algolia::VERSION', 'vIntegration')
stub_const('::Algolia::VERSION', 'vAlgolia')
stub_const('::Jekyll::VERSION', 'vJekyll')
stub_const('RUBY_VERSION', 'vRuby')
allow(::Algolia).to receive(:set_extra_header)
end
before { current.set_user_agent }
it do
expect(::Algolia)
.to have_received(:set_extra_header)
.with('User-Agent', user_agent)
end
end
describe '.index_exist?' do
let(:index) { double('Algolia::Index') }
describe 'when getting settings correctly' do
subject { current.index_exist?(index) }
before do
allow(index).to receive(:get_settings).and_return({})
end
it { should eq true }
end
describe 'when throwing an error on settings' do
subject { current.index_exist?(index) }
before do
allow(index).to receive(:get_settings).and_raise
end
it { should eq false }
end
end
describe '.record_count' do
let(:index) { double('Algolia::Index') }
let(:nb_hits) { 12 }
subject { current.record_count(index) }
describe 'when index exists' do
before do
expect(index)
.to receive(:search)
.with(
'',
hash_including(
distinct: false, # To get the correct number of records
hitsPerPage: 1, # To get a short response
attributesToRetrieve: 'objectID' # To get a short response
)
)
.and_return('nbHits' => nb_hits)
end
it { should eq 12 }
end
describe 'when index does not exist' do
before do
allow(index).to receive(:search).and_raise
end
it { should eq 0 }
end
end
describe '.remote_object_ids_from_main_index' do
let(:index) { double('Algolia::Index').as_null_object }
let(:progress_bar_instance) { double('ProgressBarInstance') }
subject { current.remote_object_ids_from_main_index }
before do
allow(current).to receive(:index).and_return(index)
allow(current).to receive(:record_count)
allow(progress_bar).to receive(:create).and_return(progress_bar_instance)
allow(progress_bar_instance).to receive(:increment)
allow(index)
.to receive(:browse)
.and_yield('objectID' => 'foo')
.and_yield('objectID' => 'bar')
end
it 'should return all objectID sorted' do
should eq %w[bar foo]
end
describe 'should grab as many ids as possible' do
before do
current.remote_object_ids_from_main_index
end
it do
expect(index)
.to have_received(:browse)
.with(
attributesToRetrieve: 'objectID',
hitsPerPage: 1000
)
end
end
describe 'should display a progress bar' do
before do
allow(current).to receive(:record_count).and_return(12)
current.remote_object_ids_from_main_index
end
it do
expect(progress_bar)
.to have_received(:create)
.with(hash_including(
total: 12
))
expect(progress_bar_instance).to have_received(:increment).twice
end
end
context 'when no index' do
before do
allow(index)
.to receive(:browse)
.and_raise
end
it { should eq [] }
end
end
describe '.remote_object_ids_from_dedicated_index' do
let(:index) { double('Algolia::Index') }
subject { current.remote_object_ids_from_dedicated_index }
before do
allow(current).to receive(:index_object_ids).and_return(index)
allow(index)
.to receive(:browse)
.and_yield('content' => %w[foo baz])
.and_yield('content' => ['bar'])
end
it 'should return all objectID sorted' do
should eq %w[bar baz foo]
end
describe 'should grab as many ids as possible' do
before do
current.remote_object_ids_from_dedicated_index
end
it do
expect(index)
.to have_received(:browse)
.with(
attributesToRetrieve: 'content',
hitsPerPage: 1000
)
end
end
context 'when no index' do
before do
allow(index)
.to receive(:browse)
.and_raise
end
it { should eq [] }
end
end
describe '.remote_object_ids' do
subject { current.remote_object_ids }
before do
allow(current)
.to receive(:remote_object_ids_from_dedicated_index)
.and_return('dedicated_results')
allow(current)
.to receive(:remote_object_ids_from_main_index)
.and_return('main_results')
allow(current).to receive(:index_object_ids).and_return('dedicated_index')
allow(current).to receive(:index).and_return('main_index')
allow(current)
.to receive(:record_count)
.with('main_index')
.and_return(main_record_count)
allow(current)
.to receive(:index_exist?)
.with('dedicated_index')
.and_return(dedicated_index_exist)
end
describe 'no index is available' do
let(:main_record_count) { 0 }
let(:dedicated_index_exist) { false }
it 'should return an empty list' do
should eq []
end
end
describe 'only main index is available' do
let(:main_record_count) { 42 }
let(:dedicated_index_exist) { false }
it 'should get objectIds from it' do
should eq 'main_results'
end
end
describe 'main index is unavailable' do
let(:main_record_count) { 0 }
let(:dedicated_index_exist) { true }
it 'should not use objectIDs from it' do
should eq []
end
end
describe 'both index are available' do
let(:main_record_count) { 42 }
let(:dedicated_index_exist) { true }
it 'should use objectIDs from the dedicated index' do
should eq 'dedicated_results'
end
end
end
describe '.local_object_ids' do
subject { current.local_object_ids(records) }
let(:records) { [{ objectID: 'foo' }, { objectID: 'bar' }] }
it { should include('foo') }
it { should include('bar') }
# Should be ordered
it { should eq %w[bar foo] }
context 'with records missing their objectID' do
let(:records) do
[
{ objectID: 'foo' },
{ foo: 'foo' },
{ objectID: 'bar' },
{ bar: 'bar' }
]
end
it { should eq %w[bar foo] }
end
end
describe '.update_records' do
let(:index) { double('Algolia::Index', name: 'main') }
let(:index_object_ids) { double('Algolia::Index', name: 'dedicated') }
let(:has_dedicated_index) { false }
let(:remote_ids) { %w[bar baz] }
let(:records) do
[
{ objectID: 'foo', name: 'foo' },
{ objectID: 'bar', name: 'bar' }
]
end
before do
allow(current).to receive(:index).and_return(index)
allow(current).to receive(:index_object_ids).and_return(index_object_ids)
allow(current).to receive(:remote_object_ids).and_return(remote_ids)
allow(current).to receive(:execute_operations)
allow(current)
.to receive(:index_exist?)
.with(index_object_ids)
.and_return(has_dedicated_index)
end
context 'when nothing to update' do
before do
allow(current).to receive(:local_object_ids).and_return(local_ids)
current.update_records(records)
end
context 'when records to update and no dedicated index' do
let(:local_ids) { ['foo'] }
let(:remote_ids) { [] }
let(:has_dedicated_index) { false }
it do
expect(current)
.to have_received(:execute_operations)
end
end
context 'when records to update and a dedicated index exist' do
let(:local_ids) { ['foo'] }
let(:remote_ids) { [] }
let(:has_dedicated_index) { true }
it do
expect(current)
.to have_received(:execute_operations)
end
end
context 'when no records to update and no dedicated index' do
let(:local_ids) { [] }
let(:remote_ids) { [] }
let(:has_dedicated_index) { false }
it do
expect(current)
.to have_received(:execute_operations)
end
end
context 'when no records to update but a dedicated index exist' do
let(:local_ids) { [] }
let(:remote_ids) { [] }
let(:has_dedicated_index) { true }
it do
expect(current)
.to_not have_received(:execute_operations)
end
end
end
context 'batch operations' do
before do
current.update_records(records)
end
it 'should start with deleting old record' do
expected = {
action: 'deleteObject',
indexName: 'main',
body: { objectID: 'baz' }
}
expect(current)
.to have_received(:execute_operations) do |operations|
expect(operations[0]).to eq expected
end
end
it 'should add new items after deleting old ones' do
expected = {
action: 'addObject',
indexName: 'main',
body: { objectID: 'foo', name: 'foo' }
}
expect(current)
.to have_received(:execute_operations) do |operations|
expect(operations[1]).to eq expected
end
end
it 'should clear the object id index after updating the record' do
expected = {
action: 'clear',
indexName: 'dedicated'
}
expect(current)
.to have_received(:execute_operations) do |operations|
expect(operations[2]).to eq expected
end
end
it 'should add new objectIDs to the dedicated index' do
expected = {
action: 'addObject',
indexName: 'dedicated',
body: { content: %w[bar foo] }
}
expect(current)
.to have_received(:execute_operations) do |operations|
expect(operations[3]).to eq expected
end
end
end
context 'when no update to the records' do
let(:local_ids) { %w[foo bar] }
let(:remote_ids) { %w[foo bar] }
before do
allow(current).to receive(:local_object_ids).and_return(local_ids)
current.update_records(records)
end
context 'do not update the dedicated index if already exist' do
let(:has_dedicated_index) { true }
it do
expect(current).to_not have_received(:execute_operations)
end
end
context 'create the dedicated index if does not yet exist' do
let(:has_dedicated_index) { false }
it do
expect(current)
.to have_received(:execute_operations) do |operations|
expect(operations[0]).to include(action: 'clear')
expect(operations[0]).to include(indexName: 'dedicated')
expect(operations[1]).to include(action: 'addObject')
expect(operations[1]).to include(body: { content: %w[foo bar] })
end
end
end
end
context 'storing ids by group of 100' do
let(:records) do
records = []
150.times { |i| records << { objectID: "foo-#{i}" } }
records
end
before do
current.update_records(records)
end
it 'should create two records for storing the object IDs' do
expect(current)
.to have_received(:execute_operations) do |operations|
dedicated_index_operations = operations.select do |operation|
operation[:indexName] == 'dedicated' &&
operation[:action] == 'addObject'
end
expect(dedicated_index_operations.length).to eq 2
end
end
end
end
describe '.execute_operations' do
let(:indexing_batch_size) { 1000 }
let(:operations) { %w[foo bar] }
let(:progress_bar_instance) { double('ProgressBarInstance') }
before do
allow(::Algolia).to receive(:batch!)
allow(progress_bar).to receive(:create).and_return(progress_bar_instance)
allow(progress_bar_instance).to receive(:increment)
allow(configurator)
.to receive(:algolia)
.with('indexing_batch_size')
.and_return(indexing_batch_size)
end
context 'when running in dry run mode' do
let(:dry_run) { true }
before { current.execute_operations(operations) }
it do
expect(::Algolia).to_not have_received(:batch!)
end
end
context 'when running an empty set of operations' do
let(:operations) { [] }
before { current.execute_operations(operations) }
it do
expect(::Algolia).to_not have_received(:batch!)
end
end
context 'split in smaller batches if too many operations' do
let(:indexing_batch_size) { 1 }
before { current.execute_operations(operations) }
it do
expect(::Algolia)
.to have_received(:batch!)
.ordered
.with(['foo'])
expect(::Algolia)
.to have_received(:batch!)
.ordered
.with(['bar'])
end
end
context 'progress bar' do
before { current.execute_operations(operations) }
describe 'should not create it if only one batch' do
it do
expect(progress_bar).to_not have_received(:create)
expect(progress_bar_instance).to_not have_received(:increment)
end
end
describe 'should create it if several batches' do
let(:indexing_batch_size) { 1 }
it do
expect(progress_bar).to have_received(:create)
expect(progress_bar_instance).to have_received(:increment).twice
end
end
end
context 'dispatch the error to the error handler' do
before do
allow(::Algolia).to receive(:batch!).and_raise
allow(error_handler).to receive(:stop)
current.execute_operations(operations)
end
describe 'when only one slice' do
it do
expect(error_handler)
.to have_received(:stop)
.with(RuntimeError, operations: operations)
end
end
describe 'when split in several slices' do
let(:indexing_batch_size) { 1 }
let(:operations) { %w[foo bar] }
it do
expect(error_handler)
.to have_received(:stop)
.with(RuntimeError, operations: ['foo'])
end
end
end
end
describe '.update_settings' do
let(:pluginVersion) { nil }
let(:diff_keys) { nil }
let(:force_settings) { nil }
let(:settings) do
Jekyll::Algolia::Configurator::ALGOLIA_DEFAULTS['settings'].merge({})
end
before do
stub_const('Jekyll::Algolia::VERSION', pluginVersion)
allow(utils).to receive(:diff_keys).and_return(diff_keys)
allow(configurator)
.to receive(:force_settings?)
.and_return(force_settings)
allow(configurator)
.to receive(:settings)
.and_return(settings)
allow(current).to receive(:set_settings)
allow(current).to receive(:warn_of_manual_dashboard_editing)
allow(current).to receive(:local_setting_id).and_return(local_setting_id)
allow(current).to receive(:remote_settings).and_return(remote_settings)
allow(current)
.to receive(:index)
.and_return(double('Algolia::index', name: 'my_index'))
current.update_settings
end
describe 'should do nothing if same settings both locally and remote' do
let(:local_setting_id) { 'foo' }
let(:remote_settings) { { 'userData' => { 'settingID' => 'foo' } } }
it { expect(current).to_not have_received(:set_settings) }
it do
expect(current).to_not have_received(:warn_of_manual_dashboard_editing)
end
context 'with remote settings manually edited' do
let(:diff_keys) { { 'foo' => 'bar' } }
it do
expect(current)
.to have_received(:warn_of_manual_dashboard_editing)
.with('foo' => 'bar')
end
end
end
describe 'should always update if --force-settings' do
let(:local_setting_id) { 'foo' }
let(:remote_settings) { { 'userData' => { 'settingID' => 'foo' } } }
let(:force_settings) { true }
it do
expect(current)
.to have_received(:set_settings)
end
end
describe 'should update settings if no remote settingID' do
let(:local_setting_id) { 'foo' }
let(:remote_settings) { { 'userData' => {} } }
it do
expect(current)
.to have_received(:set_settings)
end
end
describe 'should update settings if no remote userData' do
let(:local_setting_id) { 'foo' }
let(:remote_settings) { {} }
it do
expect(current).to have_received(:set_settings)
end
end
describe 'should update settings if no remote settings' do
let(:local_setting_id) { 'foo' }
let(:remote_settings) { nil }
it do
expect(current).to have_received(:set_settings)
end
end
describe 'should update settings if local and remote id are different' do
let(:local_setting_id) { 'foo' }
let(:remote_settings) { { 'userData' => { 'settingID' => 'bar' } } }
it do
expect(current).to have_received(:set_settings)
end
end
describe 'should update settings with new settingID' do
let(:local_setting_id) { 'foo' }
let(:remote_settings) { { 'userData' => { 'settingID' => 'bar' } } }
it do
expect(current)
.to have_received(:set_settings)
.with(
hash_including(
'userData' => hash_including('settingID' => 'foo')
)
)
end
end
describe 'should update settings with new version' do
let(:local_setting_id) { 'foo' }
let(:remote_settings) { { 'userData' => { 'settingID' => 'bar' } } }
let(:pluginVersion) { 'pluginVersion' }
it do
expect(current)
.to have_received(:set_settings)
.with(
hash_including(
'userData' => hash_including('pluginVersion' => 'pluginVersion')
)
)
end
end
describe 'should not update in dry run' do
let(:local_setting_id) { 'foo' }
let(:remote_settings) { { 'userData' => { 'settingID' => 'bar' } } }
let(:dry_run) { true }
it do
expect(current).to_not have_received(:set_settings)
end
end
describe 'should not update settings if user configured false' do
let(:local_setting_id) { 'foo' }
let(:settings) { {} }
let(:remote_settings) { {} }
it do
expect(current).to_not have_received(:set_settings)
end
end
end
describe '.local_setting_id' do
subject { current.local_setting_id }
before do
expect(configurator).to receive(:settings).and_return(settings)
end
describe do
let(:settings) { { 'foo' => 'bar' } }
it { should eq '06ad47d8e64bd28de537b62ff85357c4' }
end
describe do
let(:settings) { { 'foo' => 'baz' } }
it { should_not eq '06ad47d8e64bd28de537b62ff85357c4' }
end
describe do
let(:settings) do
{ 'foo' => 'bar', 'userData' => { 'settingID': 'foo' } }
end
it { should eq '06ad47d8e64bd28de537b62ff85357c4' }
end
end
describe '.remote_settings' do
let(:index) { double('Algolia::Index') }
before do
allow(current).to receive(:index).and_return(index)
end
context 'with actual index' do
subject { current.remote_settings }
before do
allow(index).to receive(:get_settings).and_return('settings')
end
it { should eq 'settings' }
end
context 'with API error' do
subject { current.remote_settings }
before do
allow(index).to receive(:get_settings).and_raise
end
it { is_expected.to eq nil }
end
end
describe '.set_settings' do
let(:index) { double('Algolia::Index') }
let(:settings) { 'settings' }
before do
allow(current).to receive(:index).and_return(index)
end
describe 'with valid settings' do
before do
allow(index).to receive(:set_settings!)
current.set_settings(settings)
end
it do
expect(index).to have_received(:set_settings!).with(settings)
end
end
describe 'with invalid settings' do
before do
allow(index).to receive(:set_settings!).and_raise
allow(error_handler).to receive(:stop)
current.set_settings(settings)
end
it do
expect(error_handler)
.to have_received(:stop)
.with(RuntimeError, settings: settings)
end
end
end
describe '.warn_of_manual_dashboard_editing' do
let(:changed_keys) do
{
'distinct' => false,
'customRanking' => %w[foo bar baz]
}
end
before do
allow(logger).to receive(:known_message)
allow(configurator).to receive(:index_name).and_return('my_index')
current.warn_of_manual_dashboard_editing(changed_keys)
end
it do
expect(logger)
.to have_received(:known_message)
.with(
'settings_manually_edited',
settings:
"W:    distinct: false\n"\
"W:    customRanking:\n"\
"W:      - foo\n"\
"W:      - bar\n"\
'W:      - baz',
index_name: 'my_index'
)
end
end
describe '.run' do
let(:records) { [{ objectID: 'foo' }, { objectID: 'bar' }] }
let(:remote_ids) { %w[foo baz] }
let(:index_name) { 'my_index' }
let(:index) { double('Algolia::Index', name: index_name) }
before do
allow(current).to receive(:init)
allow(current).to receive(:index).and_return(index)
allow(current).to receive(:update_settings)
allow(current).to receive(:remote_object_ids).and_return(remote_ids)
allow(current).to receive(:update_records)
end
context 'with records' do
before { current.run(records) }
it { expect(current).to have_received(:init) }
it do
expect(current)
.to have_received(:update_settings)
end
it do
expect(current)
.to have_received(:update_records)
.with(records)
end
end
context 'with empty results' do
subject { -> { current.run(records) } }
let(:records) { [] }
before do
expect(configurator)
.to receive(:algolia)
.with('files_to_exclude')
.and_return(%w[foo.html bar.md])
expect(configurator)
.to receive(:algolia)
.with('nodes_to_index')
.and_return('p,li')
expect(logger)
.to receive(:known_message)
.with(
'no_records_found',
hash_including(
'files_to_exclude' => 'foo.html, bar.md',
'nodes_to_index' => 'p,li'
)
)
end
it { is_expected.to raise_error SystemExit }
end
end
end
# rubocop:enable Metrics/BlockLength