Bug 19893: Add code review fixes
[koha.git] / t / Koha / SearchEngine / Elasticsearch.t
1 #!/usr/bin/perl
2 #
3 # This file is part of Koha.
4 #
5 # Koha is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # Koha is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18 use Modern::Perl;
19
20 use Test::More tests => 4;
21 use Test::Exception;
22
23 use t::lib::Mocks;
24
25 use Test::MockModule;
26
27 use MARC::Record;
28
29 use Koha::SearchEngine::Elasticsearch;
30
31 subtest '_read_configuration() tests' => sub {
32
33     plan tests => 10;
34
35     my $configuration;
36     t::lib::Mocks::mock_config( 'elasticsearch', undef );
37
38     # 'elasticsearch' missing in configuration
39     throws_ok {
40         $configuration = Koha::SearchEngine::Elasticsearch::_read_configuration;
41     }
42     'Koha::Exceptions::Config::MissingEntry',
43       'Configuration problem, exception thrown';
44     is(
45         $@->message,
46         "Missing 'elasticsearch' block in config file",
47         'Exception message is correct'
48     );
49
50     # 'elasticsearch' present but no 'server' entry
51     t::lib::Mocks::mock_config( 'elasticsearch', {} );
52     throws_ok {
53         $configuration = Koha::SearchEngine::Elasticsearch::_read_configuration;
54     }
55     'Koha::Exceptions::Config::MissingEntry',
56       'Configuration problem, exception thrown';
57     is(
58         $@->message,
59         "Missing 'server' entry in config file for elasticsearch",
60         'Exception message is correct'
61     );
62
63     # 'elasticsearch' and 'server' entries present, but no 'index_name'
64     t::lib::Mocks::mock_config( 'elasticsearch', { server => 'a_server' } );
65     throws_ok {
66         $configuration = Koha::SearchEngine::Elasticsearch::_read_configuration;
67     }
68     'Koha::Exceptions::Config::MissingEntry',
69       'Configuration problem, exception thrown';
70     is(
71         $@->message,
72         "Missing 'index_name' entry in config file for elasticsearch",
73         'Exception message is correct'
74     );
75
76     # Correct configuration, only one server
77     t::lib::Mocks::mock_config( 'elasticsearch',  { server => 'a_server', index_name => 'index' } );
78
79     $configuration = Koha::SearchEngine::Elasticsearch::_read_configuration;
80     is( $configuration->{index_name}, 'index', 'Index configuration parsed correctly' );
81     is_deeply( $configuration->{nodes}, ['a_server'], 'Server configuration parsed correctly' );
82
83     # Correct configuration, two servers
84     my @servers = ('a_server', 'another_server');
85     t::lib::Mocks::mock_config( 'elasticsearch', { server => \@servers, index_name => 'index' } );
86
87     $configuration = Koha::SearchEngine::Elasticsearch::_read_configuration;
88     is( $configuration->{index_name}, 'index', 'Index configuration parsed correctly' );
89     is_deeply( $configuration->{nodes}, \@servers , 'Server configuration parsed correctly' );
90 };
91
92 subtest 'get_elasticsearch_settings() tests' => sub {
93
94     plan tests => 1;
95
96     my $settings;
97
98     # test reading index settings
99     my $es = Koha::SearchEngine::Elasticsearch->new( {index => $Koha::SearchEngine::Elasticsearch::BIBLIOS_INDEX} );
100     $settings = $es->get_elasticsearch_settings();
101     is( $settings->{index}{analysis}{analyzer}{analyser_phrase}{tokenizer}, 'keyword', 'Index settings parsed correctly' );
102 };
103
104 subtest 'get_elasticsearch_mappings() tests' => sub {
105
106     plan tests => 1;
107
108     my $mappings;
109
110     # test reading mappings
111     my $es = Koha::SearchEngine::Elasticsearch->new( {index => $Koha::SearchEngine::Elasticsearch::BIBLIOS_INDEX} );
112     $mappings = $es->get_elasticsearch_mappings();
113     is( $mappings->{data}{_all}{type}, 'string', 'Field mappings parsed correctly' );
114 };
115
116 subtest 'Koha::SearchEngine::Elasticsearch::marc_records_to_documents () tests' => sub {
117
118     plan tests => 32;
119
120     t::lib::Mocks::mock_preference('marcflavour', 'MARC21');
121
122     my @mappings = (
123         {
124             name => 'author',
125             type => 'string',
126             facet => 1,
127             suggestible => 1,
128             sort => undef,
129             marc_type => 'marc21',
130             marc_field => '100a',
131         },
132         {
133             name => 'author',
134             type => 'string',
135             facet => 1,
136             suggestible => 1,
137             sort => 1,
138             marc_type => 'marc21',
139             marc_field => '110a',
140         },
141         {
142             name => 'title',
143             type => 'string',
144             facet => 0,
145             suggestible => 1,
146             sort => 1,
147             marc_type => 'marc21',
148             marc_field => '245(ab)ab',
149         },
150         {
151             name => 'unimarc_title',
152             type => 'string',
153             facet => 0,
154             suggestible => 1,
155             sort => 1,
156             marc_type => 'unimarc',
157             marc_field => '245a',
158         },
159         {
160             name => 'title',
161             type => 'string',
162             facet => 0,
163             suggestible => undef,
164             sort => 0,
165             marc_type => 'marc21',
166             marc_field => '220',
167         },
168         {
169             name => 'sum_item_price',
170             type => 'sum',
171             facet => 0,
172             suggestible => 0,
173             sort => 0,
174             marc_type => 'marc21',
175             marc_field => '952g',
176         },
177         {
178             name => 'items_withdrawn_status',
179             type => 'boolean',
180             facet => 0,
181             suggestible => 0,
182             sort => 0,
183             marc_type => 'marc21',
184             marc_field => '9520',
185         },
186         {
187             name => 'type_of_record',
188             type => 'string',
189             facet => 0,
190             suggestible => 0,
191             sort => 0,
192             marc_type => 'marc21',
193             marc_field => 'leader_/6',
194         },
195         {
196             name => 'type_of_record_and_bib_level',
197             type => 'string',
198             facet => 0,
199             suggestible => 0,
200             sort => 0,
201             marc_type => 'marc21',
202             marc_field => 'leader_/6-7',
203         },
204     );
205
206     my $se = Test::MockModule->new('Koha::SearchEngine::Elasticsearch');
207     $se->mock('_foreach_mapping', sub {
208         my ($self, $sub) = @_;
209
210         foreach my $map (@mappings) {
211             $sub->(
212                 $map->{name},
213                 $map->{type},
214                 $map->{facet},
215                 $map->{suggestible},
216                 $map->{sort},
217                 $map->{marc_type},
218                 $map->{marc_field}
219             );
220         }
221     });
222
223     my $see = Koha::SearchEngine::Elasticsearch->new({ index => $Koha::SearchEngine::Elasticsearch::BIBLIOS_INDEX });
224
225     my $marc_record_1 = MARC::Record->new();
226     $marc_record_1->leader('     cam  22      a 4500');
227     $marc_record_1->append_fields(
228         MARC::Field->new('100', '', '', a => 'Author 1'),
229         MARC::Field->new('110', '', '', a => 'Corp Author'),
230         MARC::Field->new('210', '', '', a => 'Title 1'),
231         MARC::Field->new('245', '', '', a => 'Title:', b => 'first record'),
232         MARC::Field->new('999', '', '', c => '1234567'),
233         # '  ' for testing trimming of white space in boolean value callback:
234         MARC::Field->new('952', '', '', 0 => '  ', g => '123.30'),
235         MARC::Field->new('952', '', '', 0 => 0, g => '127.20'),
236     );
237     my $marc_record_2 = MARC::Record->new();
238     $marc_record_2->leader('     cam  22      a 4500');
239     $marc_record_2->append_fields(
240         MARC::Field->new('100', '', '', a => 'Author 2'),
241         # MARC::Field->new('210', '', '', a => 'Title 2'),
242         # MARC::Field->new('245', '', '', a => 'Title: second record'),
243         MARC::Field->new('999', '', '', c => '1234568'),
244         MARC::Field->new('952', '', '', 0 => 1, g => 'string where should be numeric'),
245     );
246     my $records = [$marc_record_1, $marc_record_2];
247
248     $see->get_elasticsearch_mappings(); #sort_fields will call this and use the actual db values unless we call it first
249
250     my $docs = $see->marc_records_to_documents($records);
251
252     # First record:
253
254     is(scalar @{$docs}, 2, 'Two records converted to documents');
255
256     is($docs->[0][0], '1234567', 'First document biblionumber should be set as first element in document touple');
257
258     is(scalar @{$docs->[0][1]->{author}}, 2, 'First document author field should contain two values');
259     is_deeply($docs->[0][1]->{author}, ['Author 1', 'Corp Author'], 'First document author field should be set correctly');
260
261     is(scalar @{$docs->[0][1]->{author__sort}}, 2, 'First document author__sort field should have two values');
262     is_deeply($docs->[0][1]->{author__sort}, ['Author 1', 'Corp Author'], 'First document author__sort field should be set correctly');
263
264     is(scalar @{$docs->[0][1]->{title__sort}}, 3, 'First document title__sort field should have three values');
265     is_deeply($docs->[0][1]->{title__sort}, ['Title:', 'first record', 'Title: first record'], 'First document title__sort field should be set correctly');
266
267     is(scalar @{$docs->[0][1]->{author__suggestion}}, 2, 'First document author__suggestion field should contain two values');
268     is_deeply(
269         $docs->[0][1]->{author__suggestion},
270         [
271             {
272                 'input' => 'Author 1'
273             },
274             {
275                 'input' => 'Corp Author'
276             }
277         ],
278         'First document author__suggestion field should be set correctly'
279     );
280
281     is(scalar @{$docs->[0][1]->{title__suggestion}}, 3, 'First document title__suggestion field should contain three values');
282     is_deeply(
283         $docs->[0][1]->{title__suggestion},
284         [
285             { 'input' => 'Title:' },
286             { 'input' => 'first record' },
287             { 'input' => 'Title: first record' }
288         ],
289         'First document title__suggestion field should be set correctly'
290     );
291
292     ok(!(defined $docs->[0][1]->{title__facet}), 'First document should have no title__facet field');
293
294     is(scalar @{$docs->[0][1]->{author__facet}}, 2, 'First document author__facet field should have two values');
295     is_deeply(
296         $docs->[0][1]->{author__facet},
297         ['Author 1', 'Corp Author'],
298         'First document author__facet field should be set correctly'
299     );
300
301     is(scalar @{$docs->[0][1]->{items_withdrawn_status}}, 2, 'First document items_withdrawn_status field should have two values');
302     is_deeply(
303         $docs->[0][1]->{items_withdrawn_status},
304         ['false', 'false'],
305         'First document items_withdrawn_status field should be set correctly'
306     );
307
308     is(
309         $docs->[0][1]->{sum_item_price},
310         '250.5',
311         'First document sum_item_price field should be set correctly'
312     );
313
314     ok(defined $docs->[0][1]->{marc_data}, 'First document marc_data field should be set');
315
316     ok(defined $docs->[0][1]->{marc_format}, 'First document marc_format field should be set');
317
318     is($docs->[0][1]->{marc_format}, 'base64ISO2709', 'First document marc_format should be set correctly');
319
320     is(scalar @{$docs->[0][1]->{type_of_record}}, 1, 'First document type_of_record field should have one value');
321     is_deeply(
322         $docs->[0][1]->{type_of_record},
323         ['a'],
324         'First document type_of_record field should be set correctly'
325     );
326
327     is(scalar @{$docs->[0][1]->{type_of_record_and_bib_level}}, 1, 'First document type_of_record_and_bib_level field should have one value');
328     is_deeply(
329         $docs->[0][1]->{type_of_record_and_bib_level},
330         ['am'],
331         'First document type_of_record_and_bib_level field should be set correctly'
332     );
333
334     # Second record:
335
336     is(scalar @{$docs->[1][1]->{author}}, 1, 'Second document author field should contain one value');
337     is_deeply($docs->[1][1]->{author}, ['Author 2'], 'Second document author field should be set correctly');
338
339     is(scalar @{$docs->[1][1]->{items_withdrawn_status}}, 1, 'Second document items_withdrawn_status field should have one value');
340     is_deeply(
341         $docs->[1][1]->{items_withdrawn_status},
342         ['true'],
343         'Second document items_withdrawn_status field should be set correctly'
344     );
345
346     is(
347         $docs->[1][1]->{sum_item_price},
348         0,
349         'Second document sum_item_price field should be set correctly'
350     );
351
352     # Mappings marc_type:
353
354     ok(!(defined $docs->[0][1]->{unimarc_title}), "No mapping when marc_type doesn't match marc flavour");
355
356     # Marc serialization format fallback for records exceeding ISO2709 max record size
357
358     my $large_marc_record = MARC::Record->new();
359     $large_marc_record->leader('     cam  22      a 4500');
360
361     $large_marc_record->append_fields(
362         MARC::Field->new('100', '', '', a => 'Author 1'),
363         MARC::Field->new('110', '', '', a => 'Corp Author'),
364         MARC::Field->new('210', '', '', a => 'Title 1'),
365         MARC::Field->new('245', '', '', a => 'Title:', b => 'large record'),
366         MARC::Field->new('999', '', '', c => '1234567'),
367     );
368
369     my $item_field = MARC::Field->new('952', '', '', o => '123456789123456789123456789', p => '123456789', z => 'test');
370     my $items_count = 1638;
371     while(--$items_count) {
372         $large_marc_record->append_fields($item_field);
373     }
374
375     $docs = $see->marc_records_to_documents([$large_marc_record]);
376
377     is($docs->[0][1]->{marc_format}, 'MARCXML', 'For record exceeding max record size marc_format should be set correctly');
378
379 };