Bug 27784: Unit tests
[koha.git] / t / db_dependent / 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 => 6;
21 use Test::Exception;
22
23 use t::lib::Mocks;
24 use t::lib::TestBuilder;
25
26 use Test::MockModule;
27
28 use MARC::Record;
29 use Try::Tiny;
30 use List::Util qw( any );
31
32 use Koha::SearchEngine::Elasticsearch;
33 use Koha::SearchEngine::Elasticsearch::Search;
34
35 my $schema = Koha::Database->new->schema;
36 $schema->storage->txn_begin;
37
38 subtest '_read_configuration() tests' => sub {
39
40     plan tests => 15;
41
42     my $configuration;
43     t::lib::Mocks::mock_config( 'elasticsearch', undef );
44
45     # 'elasticsearch' missing in configuration
46     throws_ok {
47         $configuration = Koha::SearchEngine::Elasticsearch::_read_configuration;
48     }
49     'Koha::Exceptions::Config::MissingEntry',
50       'Configuration problem, exception thrown';
51     is(
52         $@->message,
53         "Missing <elasticsearch> entry in koha-conf.xml",
54         'Exception message is correct'
55     );
56
57     # 'elasticsearch' present but no 'server' entry
58     t::lib::Mocks::mock_config( 'elasticsearch', {} );
59     throws_ok {
60         $configuration = Koha::SearchEngine::Elasticsearch::_read_configuration;
61     }
62     'Koha::Exceptions::Config::MissingEntry',
63       'Configuration problem, exception thrown';
64     is(
65         $@->message,
66         "Missing <elasticsearch>/<server> entry in koha-conf.xml",
67         'Exception message is correct'
68     );
69
70     # 'elasticsearch' and 'server' entries present, but no 'index_name'
71     t::lib::Mocks::mock_config( 'elasticsearch', { server => 'a_server' } );
72     throws_ok {
73         $configuration = Koha::SearchEngine::Elasticsearch::_read_configuration;
74     }
75     'Koha::Exceptions::Config::MissingEntry',
76       'Configuration problem, exception thrown';
77     is(
78         $@->message,
79         "Missing <elasticsearch>/<index_name> entry in koha-conf.xml",
80         'Exception message is correct'
81     );
82
83     # Correct configuration, only one server
84     t::lib::Mocks::mock_config( 'elasticsearch',  { server => 'a_server', index_name => 'index' } );
85
86     $configuration = Koha::SearchEngine::Elasticsearch::_read_configuration;
87     is( $configuration->{index_name}, 'index', 'Index configuration parsed correctly' );
88     is_deeply( $configuration->{nodes}, ['a_server'], 'Server configuration parsed correctly' );
89
90     # Correct configuration, two servers
91     my @servers = ('a_server', 'another_server');
92     t::lib::Mocks::mock_config( 'elasticsearch', { server => \@servers, index_name => 'index' } );
93
94     $configuration = Koha::SearchEngine::Elasticsearch::_read_configuration;
95     is( $configuration->{index_name}, 'index', 'Index configuration parsed correctly' );
96     is( $configuration->{cxn_pool}, 'Static', 'cxn_pool configuration set correctly to Static if not specified' );
97     is_deeply( $configuration->{nodes}, \@servers , 'Server configuration parsed correctly' );
98
99     t::lib::Mocks::mock_config( 'elasticsearch', { server => \@servers, index_name => 'index', cxn_pool => 'Fluid' } );
100
101     $configuration = Koha::SearchEngine::Elasticsearch::_read_configuration;
102     is( $configuration->{cxn_pool}, 'Fluid', 'cxn_pool configuration parsed correctly' );
103     isnt( defined $configuration->{trace_to}, 'trace_to is not defined if not set' );
104
105     my $params = Koha::SearchEngine::Elasticsearch::get_elasticsearch_params;
106     is_deeply( $configuration->{nodes}, \@servers , 'get_elasticsearch_params is just a wrapper for _read_configuration' );
107
108     t::lib::Mocks::mock_config( 'elasticsearch', { server => \@servers, index_name => 'index', cxn_pool => 'Fluid', trace_to => 'Stderr' } );
109
110     $configuration = Koha::SearchEngine::Elasticsearch::_read_configuration;
111     is( $configuration->{trace_to}, 'Stderr', 'trace_to configuration parsed correctly' );
112 };
113
114 subtest 'get_elasticsearch_settings() tests' => sub {
115
116     plan tests => 1;
117
118     my $settings;
119
120     # test reading index settings
121     my $es = Koha::SearchEngine::Elasticsearch->new( {index => $Koha::SearchEngine::Elasticsearch::BIBLIOS_INDEX} );
122     $settings = $es->get_elasticsearch_settings();
123     is( $settings->{index}{analysis}{analyzer}{analyzer_phrase}{tokenizer}, 'keyword', 'Index settings parsed correctly' );
124 };
125
126 subtest 'get_elasticsearch_mappings() tests' => sub {
127
128     plan tests => 1;
129
130     my $mappings;
131
132     # test reading mappings
133     my $es = Koha::SearchEngine::Elasticsearch->new( {index => $Koha::SearchEngine::Elasticsearch::BIBLIOS_INDEX} );
134     $mappings = $es->get_elasticsearch_mappings();
135     is( $mappings->{data}{properties}{isbn__sort}{index}, 'false', 'Field mappings parsed correctly' );
136 };
137
138 subtest 'Koha::SearchEngine::Elasticsearch::marc_records_to_documents () tests' => sub {
139
140     plan tests => 63;
141
142     t::lib::Mocks::mock_preference('marcflavour', 'MARC21');
143     t::lib::Mocks::mock_preference('ElasticsearchMARCFormat', 'ISO2709');
144
145     my @mappings = (
146         {
147             name => 'control_number',
148             type => 'string',
149             facet => 0,
150             suggestible => 0,
151             searchable => 1,
152             sort => undef,
153             marc_type => 'marc21',
154             marc_field => '001',
155         },
156         {
157             name => 'isbn',
158             type => 'isbn',
159             facet => 0,
160             suggestible => 0,
161             searchable => 1,
162             sort => 0,
163             marc_type => 'marc21',
164             marc_field => '020a',
165         },
166         {
167             name => 'author',
168             type => 'string',
169             facet => 1,
170             suggestible => 1,
171             searchable => 1,
172             sort => undef,
173             marc_type => 'marc21',
174             marc_field => '100a',
175         },
176         {
177             name => 'author',
178             type => 'string',
179             facet => 1,
180             suggestible => 1,
181             searchable => 1,
182             sort => 1,
183             marc_type => 'marc21',
184             marc_field => '110a',
185         },
186         {
187             name => 'title',
188             type => 'string',
189             facet => 0,
190             suggestible => 1,
191             searchable => 1,
192             sort => 1,
193             marc_type => 'marc21',
194             marc_field => '245(ab)ab',
195         },
196         {
197             name => 'unimarc_title',
198             type => 'string',
199             facet => 0,
200             suggestible => 1,
201             searchable => 1,
202             sort => 1,
203             marc_type => 'unimarc',
204             marc_field => '245a',
205         },
206         {
207             name => 'title',
208             type => 'string',
209             facet => 0,
210             suggestible => undef,
211             searchable => 1,
212             sort => 0,
213             marc_type => 'marc21',
214             marc_field => '220',
215         },
216         {
217             name => 'uniform_title',
218             type => 'string',
219             facet => 0,
220             suggestible => 0,
221             searchable => 1,
222             sort => 1,
223             marc_type => 'marc21',
224             marc_field => '240a',
225         },
226         {
227             name => 'title_wildcard',
228             type => 'string',
229             facet => 0,
230             suggestible => 0,
231             searchable => 1,
232             sort => undef,
233             marc_type => 'marc21',
234             marc_field => '245',
235         },
236         {
237             name => 'sum_item_price',
238             type => 'sum',
239             facet => 0,
240             suggestible => 0,
241             searchable => 1,
242             sort => 0,
243             marc_type => 'marc21',
244             marc_field => '952g',
245         },
246         {
247             name => 'items_withdrawn_status',
248             type => 'boolean',
249             facet => 0,
250             suggestible => 0,
251             searchable => 1,
252             sort => 0,
253             marc_type => 'marc21',
254             marc_field => '9520',
255         },
256         {
257             name => 'local_classification',
258             type => 'string',
259             facet => 0,
260             suggestible => 0,
261             searchable => 1,
262             sort => 1,
263             marc_type => 'marc21',
264             marc_field => '952o',
265         },
266         {
267             name => 'type_of_record',
268             type => 'string',
269             facet => 0,
270             suggestible => 0,
271             searchable => 1,
272             sort => 0,
273             marc_type => 'marc21',
274             marc_field => 'leader_/6',
275         },
276         {
277             name => 'type_of_record_and_bib_level',
278             type => 'string',
279             facet => 0,
280             suggestible => 0,
281             searchable => 1,
282             sort => 0,
283             marc_type => 'marc21',
284             marc_field => 'leader_/6-7',
285         },
286         {
287             name => 'ff7-00',
288             type => 'string',
289             facet => 0,
290             suggestible => 0,
291             searchable => 1,
292             sort => 0,
293             marc_type => 'marc21',
294             marc_field => '007_/0',
295         },
296         {
297             name => 'issues',
298             type => 'sum',
299             facet => 0,
300             suggestible => 0,
301             searchable => 1,
302             sort => 1,
303             marc_type => 'marc21',
304             marc_field => '952l',
305           },
306           {
307             name => 'copydate',
308             type => 'year',
309             facet => 0,
310             suggestible => 0,
311             searchable => 1,
312             sort => 1,
313             marc_type => 'marc21',
314             marc_field => '260c',
315           },
316           {
317             name => 'date-of-publication',
318             type => 'year',
319             facet => 0,
320             suggestible => 0,
321             searchable => 1,
322             sort => 1,
323             marc_type => 'marc21',
324             marc_field => '008_/7-10',
325         },
326         {
327             name => 'subject',
328             type => 'string',
329             facet => 0,
330             suggestible => 0,
331             searchable => 1,
332             sort => 1,
333             marc_type => 'marc21',
334             marc_field => '650(avxyz)',
335         },
336     );
337
338     my $se = Test::MockModule->new('Koha::SearchEngine::Elasticsearch');
339     $se->mock('_foreach_mapping', sub {
340         my ($self, $sub) = @_;
341
342         foreach my $map (@mappings) {
343             $sub->(
344                 $map->{name},
345                 $map->{type},
346                 $map->{facet},
347                 $map->{suggestible},
348                 $map->{sort},
349                 $map->{searchable},
350                 $map->{marc_type},
351                 $map->{marc_field}
352             );
353         }
354     });
355
356     my $see = Koha::SearchEngine::Elasticsearch::Search->new({ index => $Koha::SearchEngine::Elasticsearch::BIBLIOS_INDEX });
357
358     my $callno = 'ABC123';
359     my $callno2 = 'ABC456';
360     my $long_callno = '1234567890' x 30;
361
362     my $marc_record_1 = MARC::Record->new();
363     $marc_record_1->leader('     cam  22      a 4500');
364     $marc_record_1->append_fields(
365         MARC::Field->new('001', '123'),
366         MARC::Field->new('007', 'ku'),
367         MARC::Field->new('008', '901111s1962 xxk|||| |00| ||eng c'),
368         MARC::Field->new('020', '', '', a => '1-56619-909-3'),
369         MARC::Field->new('100', '', '', a => 'Author 1'),
370         MARC::Field->new('110', '', '', a => 'Corp Author'),
371         MARC::Field->new('210', '', '', a => 'Title 1'),
372         MARC::Field->new('240', '', '4', a => 'The uniform title with nonfiling indicator'),
373         MARC::Field->new('245', '', '', a => 'Title:', b => 'first record'),
374         MARC::Field->new('260', '', '', a => 'New York :', b => 'Ace ,', c => 'c1962'),
375         MARC::Field->new('650', '', '', a => 'Heading', z => 'Geohead', v => 'Formhead'),
376         MARC::Field->new('650', '', '', a => 'Heading', x => 'Gensubhead', z => 'Geohead'),
377         MARC::Field->new('999', '', '', c => '1234567'),
378         # '  ' for testing trimming of white space in boolean value callback:
379         MARC::Field->new('952', '', '', 0 => '  ', g => '123.30', o => $callno, l => 3),
380         MARC::Field->new('952', '', '', 0 => 0, g => '127.20', o => $callno2, l => 2),
381         MARC::Field->new('952', '', '', 0 => 1, g => '0.00', o => $long_callno, l => 1),
382     );
383     my $marc_record_2 = MARC::Record->new();
384     $marc_record_2->leader('     cam  22      a 4500');
385     $marc_record_2->append_fields(
386         MARC::Field->new('008', '901111s19uu xxk|||| |00| ||eng c'),
387         MARC::Field->new('100', '', '', a => 'Author 2'),
388         # MARC::Field->new('210', '', '', a => 'Title 2'),
389         # MARC::Field->new('245', '', '', a => 'Title: second record'),
390         MARC::Field->new('260', '', '', a => 'New York :', b => 'Ace ,', c => '1963-2003'),
391         MARC::Field->new('999', '', '', c => '1234568'),
392         MARC::Field->new('952', '', '', 0 => 1, g => 'string where should be numeric', o => $long_callno),
393     );
394
395     my $marc_record_3 = MARC::Record->new();
396     $marc_record_3->leader('     cam  22      a 4500');
397     $marc_record_3->append_fields(
398         MARC::Field->new('008', '901111s19uu xxk|||| |00| ||eng c'),
399         MARC::Field->new('100', '', '', a => 'Author 2'),
400         # MARC::Field->new('210', '', '', a => 'Title 3'),
401         # MARC::Field->new('245', '', '', a => 'Title: third record'),
402         MARC::Field->new('260', '', '', a => 'New York :', b => 'Ace ,', c => ' 89 '),
403         MARC::Field->new('999', '', '', c => '1234568'),
404         MARC::Field->new('952', '', '', 0 => 1, g => 'string where should be numeric', o => $long_callno),
405     );
406     my $records = [$marc_record_1, $marc_record_2, $marc_record_3];
407
408     $see->get_elasticsearch_mappings(); #sort_fields will call this and use the actual db values unless we call it first
409
410     my $docs = $see->marc_records_to_documents($records);
411
412     # First record:
413     is(scalar @{$docs}, 3, 'Two records converted to documents');
414
415     is_deeply($docs->[0]->{control_number}, ['123'], 'First record control number should be set correctly');
416
417     is_deeply($docs->[0]->{'ff7-00'}, ['k'], 'First record ff7-00 should be set correctly');
418
419     is(scalar @{$docs->[0]->{author}}, 2, 'First document author field should contain two values');
420     is_deeply($docs->[0]->{author}, ['Author 1', 'Corp Author'], 'First document author field should be set correctly');
421
422     is(scalar @{$docs->[0]->{subject}}, 2, 'First document subject field should contain two values');
423     is_deeply($docs->[0]->{subject}, ['Heading Geohead Formhead', 'Heading Gensubhead Geohead'], 'First document asubject field should be set correctly, record order preserved for grouped subfield mapping');
424
425     is(scalar @{$docs->[0]->{author__sort}}, 1, 'First document author__sort field should have a single value');
426     is_deeply($docs->[0]->{author__sort}, ['Author 1 Corp Author'], 'First document author__sort field should be set correctly');
427
428     is(scalar @{$docs->[0]->{title__sort}}, 1, 'First document title__sort field should have a single');
429     is_deeply($docs->[0]->{title__sort}, ['Title: first record Title: first record'], 'First document title__sort field should be set correctly');
430
431     is($docs->[0]->{issues}, 6, 'Issues field should be sum of the issues for each item');
432     is($docs->[0]->{issues__sort}, 6, 'Issues sort field should also be a sum of the issues');
433
434     is(scalar @{$docs->[0]->{title_wildcard}}, 2, 'First document title_wildcard field should have two values');
435     is_deeply($docs->[0]->{title_wildcard}, ['Title:', 'first record'], 'First document title_wildcard field should be set correctly');
436
437
438     is(scalar @{$docs->[0]->{author__suggestion}}, 2, 'First document author__suggestion field should contain two values');
439     is_deeply(
440         $docs->[0]->{author__suggestion},
441         [
442             {
443                 'input' => 'Author 1'
444             },
445             {
446                 'input' => 'Corp Author'
447             }
448         ],
449         'First document author__suggestion field should be set correctly'
450     );
451
452     is(scalar @{$docs->[0]->{title__suggestion}}, 3, 'First document title__suggestion field should contain three values');
453     is_deeply(
454         $docs->[0]->{title__suggestion},
455         [
456             { 'input' => 'Title:' },
457             { 'input' => 'first record' },
458             { 'input' => 'Title: first record' }
459         ],
460         'First document title__suggestion field should be set correctly'
461     );
462
463     ok(!(defined $docs->[0]->{title__facet}), 'First document should have no title__facet field');
464
465     is(scalar @{$docs->[0]->{author__facet}}, 2, 'First document author__facet field should have two values');
466     is_deeply(
467         $docs->[0]->{author__facet},
468         ['Author 1', 'Corp Author'],
469         'First document author__facet field should be set correctly'
470     );
471
472     is(scalar @{$docs->[0]->{items_withdrawn_status}}, 2, 'First document items_withdrawn_status field should have two values');
473     is_deeply(
474         $docs->[0]->{items_withdrawn_status},
475         ['false', 'true'],
476         'First document items_withdrawn_status field should be set correctly'
477     );
478
479     is(
480         $docs->[0]->{sum_item_price},
481         '250.5',
482         'First document sum_item_price field should be set correctly'
483     );
484
485     ok(defined $docs->[0]->{marc_data}, 'First document marc_data field should be set');
486     ok(defined $docs->[0]->{marc_format}, 'First document marc_format field should be set');
487     is($docs->[0]->{marc_format}, 'base64ISO2709', 'First document marc_format should be set correctly');
488
489     my $decoded_marc_record = $see->decode_record_from_result($docs->[0]);
490
491     ok($decoded_marc_record->isa('MARC::Record'), "base64ISO2709 record successfully decoded from result");
492     is($decoded_marc_record->as_usmarc(), $marc_record_1->as_usmarc(), "Decoded base64ISO2709 record has same data as original record");
493
494     is(scalar @{$docs->[0]->{type_of_record}}, 1, 'First document type_of_record field should have one value');
495     is_deeply(
496         $docs->[0]->{type_of_record},
497         ['a'],
498         'First document type_of_record field should be set correctly'
499     );
500
501     is(scalar @{$docs->[0]->{type_of_record_and_bib_level}}, 1, 'First document type_of_record_and_bib_level field should have one value');
502     is_deeply(
503         $docs->[0]->{type_of_record_and_bib_level},
504         ['am'],
505         'First document type_of_record_and_bib_level field should be set correctly'
506     );
507
508     is(scalar @{$docs->[0]->{isbn}}, 4, 'First document isbn field should contain four values');
509     is_deeply($docs->[0]->{isbn}, ['978-1-56619-909-4', '9781566199094', '1-56619-909-3', '1566199093'], 'First document isbn field should be set correctly');
510
511     is_deeply(
512         $docs->[0]->{'local_classification'},
513         [$callno, $callno2, $long_callno],
514         'First document local_classification field should be set correctly'
515     );
516
517     # Nonfiling characters for sort fields
518     is_deeply(
519         $docs->[0]->{uniform_title},
520         ['The uniform title with nonfiling indicator'],
521         'First document uniform_title field should contain the title verbatim'
522     );
523     is_deeply(
524         $docs->[0]->{uniform_title__sort},
525         ['uniform title with nonfiling indicator'],
526         'First document uniform_title__sort field should contain the title with the first four initial characters removed'
527     );
528
529     # Tests for 'year' type
530     is(scalar @{$docs->[0]->{'date-of-publication'}}, 1, 'First document date-of-publication field should contain one value');
531     is_deeply($docs->[0]->{'date-of-publication'}, ['1962'], 'First document date-of-publication field should be set correctly');
532
533     is_deeply(
534       $docs->[0]->{'copydate'},
535       ['1962'],
536       'First document copydate field should be set correctly'
537     );
538
539     # Second record:
540
541     is(scalar @{$docs->[1]->{author}}, 1, 'Second document author field should contain one value');
542     is_deeply($docs->[1]->{author}, ['Author 2'], 'Second document author field should be set correctly');
543
544     is(scalar @{$docs->[1]->{items_withdrawn_status}}, 1, 'Second document items_withdrawn_status field should have one value');
545     is_deeply(
546         $docs->[1]->{items_withdrawn_status},
547         ['true'],
548         'Second document items_withdrawn_status field should be set correctly'
549     );
550
551     is(
552         $docs->[1]->{sum_item_price},
553         0,
554         'Second document sum_item_price field should be set correctly'
555     );
556
557     is_deeply(
558         $docs->[1]->{local_classification__sort},
559         [substr($long_callno, 0, 255)],
560         'Second document local_classification__sort field should be set correctly'
561     );
562
563     # Tests for 'year' type
564     is_deeply(
565       $docs->[1]->{'copydate'},
566       ['1963', '2003'],
567       'Second document copydate field should be set correctly'
568     );
569     is_deeply(
570       $docs->[1]->{'date-of-publication'},
571       ['1900'],
572       'Second document date-of-publication field should be set correctly'
573     );
574
575     # Third record:
576
577     is_deeply(
578       $docs->[2]->{'copydate'},
579       ['0890'],
580       'Third document copydate field should be set correctly'
581     );
582
583     # Mappings marc_type:
584
585     ok(!(defined $docs->[0]->{unimarc_title}), "No mapping when marc_type doesn't match marc flavour");
586
587     # Marc serialization format fallback for records exceeding ISO2709 max record size
588
589     my $large_marc_record = MARC::Record->new();
590     $large_marc_record->leader('     cam  22      a 4500');
591
592     $large_marc_record->append_fields(
593         MARC::Field->new('100', '', '', a => 'Author 1'),
594         MARC::Field->new('110', '', '', a => 'Corp Author'),
595         MARC::Field->new('210', '', '', a => 'Title 1'),
596         MARC::Field->new('245', '', '', a => 'Title:', b => 'large record'),
597         MARC::Field->new('999', '', '', c => '1234567'),
598     );
599
600     my $item_field = MARC::Field->new('952', '', '', o => '123456789123456789123456789', p => '123456789', z => 'test');
601     my $items_count = 1638;
602     while(--$items_count) {
603         $large_marc_record->append_fields($item_field);
604     }
605
606     $docs = $see->marc_records_to_documents([$large_marc_record]);
607
608     is($docs->[0]->{marc_format}, 'MARCXML', 'For record exceeding max record size marc_format should be set correctly');
609
610     $decoded_marc_record = $see->decode_record_from_result($docs->[0]);
611
612     ok($decoded_marc_record->isa('MARC::Record'), "MARCXML record successfully decoded from result");
613     is($decoded_marc_record->as_xml_record(), $large_marc_record->as_xml_record(), "Decoded MARCXML record has same data as original record");
614
615     push @mappings, {
616         name => 'title',
617         type => 'string',
618         facet => 0,
619         suggestible => 1,
620         sort => 1,
621         marc_type => 'marc21',
622         marc_field => '245((ab)ab',
623     };
624
625     my $exception = try {
626         $see->marc_records_to_documents($records);
627     }
628     catch {
629         return $_;
630     };
631
632     ok(defined $exception, "Exception has been thrown when processing mapping with unmatched opening parenthesis");
633     ok($exception->isa("Koha::Exceptions::Elasticsearch::MARCFieldExprParseError"), "Exception is of correct class");
634     ok($exception->message =~ /Unmatched opening parenthesis/, "Exception has the correct message");
635
636     pop @mappings;
637     push @mappings, {
638         name => 'title',
639         type => 'string',
640         facet => 0,
641         suggestible => 1,
642         sort => 1,
643         marc_type => 'marc21',
644         marc_field => '245(ab))ab',
645     };
646
647     $exception = try {
648         $see->marc_records_to_documents($records);
649     }
650     catch {
651         return $_;
652     };
653
654     ok(defined $exception, "Exception has been thrown when processing mapping with unmatched closing parenthesis");
655     ok($exception->isa("Koha::Exceptions::Elasticsearch::MARCFieldExprParseError"), "Exception is of correct class");
656     ok($exception->message =~ /Unmatched closing parenthesis/, "Exception has the correct message");
657
658     pop @mappings;
659     my $marc_record_with_blank_field = MARC::Record->new();
660     $marc_record_with_blank_field->leader('     cam  22      a 4500');
661
662     $marc_record_with_blank_field->append_fields(
663         MARC::Field->new('100', '', '', a => ''),
664         MARC::Field->new('210', '', '', a => 'Title 1'),
665         MARC::Field->new('245', '', '', a => 'Title:', b => 'large record'),
666         MARC::Field->new('999', '', '', c => '1234567'),
667     );
668     $docs = $see->marc_records_to_documents([$marc_record_with_blank_field]);
669     is_deeply( $docs->[0]->{author},[],'No value placed into field if mapped marc field is blank');
670     is_deeply( $docs->[0]->{author__suggestion},[],'No value placed into suggestion if mapped marc field is blank');
671
672 };
673
674 subtest 'Koha::SearchEngine::Elasticsearch::marc_records_to_documents_array () tests' => sub {
675
676     plan tests => 5;
677
678     t::lib::Mocks::mock_preference('marcflavour', 'MARC21');
679     t::lib::Mocks::mock_preference('ElasticsearchMARCFormat', 'ARRAY');
680
681     my @mappings = (
682         {
683             name => 'control_number',
684             type => 'string',
685             facet => 0,
686             suggestible => 0,
687             sort => undef,
688             searchable => 1,
689             marc_type => 'marc21',
690             marc_field => '001',
691         }
692     );
693
694     my $se = Test::MockModule->new('Koha::SearchEngine::Elasticsearch');
695     $se->mock('_foreach_mapping', sub {
696         my ($self, $sub) = @_;
697
698         foreach my $map (@mappings) {
699             $sub->(
700                 $map->{name},
701                 $map->{type},
702                 $map->{facet},
703                 $map->{suggestible},
704                 $map->{sort},
705                 $map->{searchable},
706                 $map->{marc_type},
707                 $map->{marc_field}
708             );
709         }
710     });
711
712     my $see = Koha::SearchEngine::Elasticsearch::Search->new({ index => $Koha::SearchEngine::Elasticsearch::BIBLIOS_INDEX });
713
714     my $marc_record_1 = MARC::Record->new();
715     $marc_record_1->leader('     cam  22      a 4500');
716     $marc_record_1->append_fields(
717         MARC::Field->new('001', '123'),
718         MARC::Field->new('020', '', '', a => '1-56619-909-3'),
719         MARC::Field->new('100', '', '', a => 'Author 1'),
720         MARC::Field->new('110', '', '', a => 'Corp Author'),
721         MARC::Field->new('210', '', '', a => 'Title 1'),
722         MARC::Field->new('245', '', '', a => 'Title:', b => 'first record'),
723         MARC::Field->new('999', '', '', c => '1234567'),
724     );
725     my $marc_record_2 = MARC::Record->new();
726     $marc_record_2->leader('     cam  22      a 4500');
727     $marc_record_2->append_fields(
728         MARC::Field->new('100', '', '', a => 'Author 2'),
729         # MARC::Field->new('210', '', '', a => 'Title 2'),
730         # MARC::Field->new('245', '', '', a => 'Title: second record'),
731         MARC::Field->new('999', '', '', c => '1234568'),
732         MARC::Field->new('952', '', '', 0 => 1, g => 'string where should be numeric'),
733     );
734     my $records = [ $marc_record_1, $marc_record_2 ];
735
736     $see->get_elasticsearch_mappings(); #sort_fields will call this and use the actual db values unless we call it first
737
738     my $docs = $see->marc_records_to_documents($records);
739
740     # First record:
741     is(scalar @{$docs}, 2, 'Two records converted to documents');
742
743     is_deeply($docs->[0]->{control_number}, ['123'], 'First record control number should be set correctly');
744
745     is($docs->[0]->{marc_format}, 'ARRAY', 'First document marc_format should be set correctly');
746
747     my $decoded_marc_record = $see->decode_record_from_result($docs->[0]);
748
749     ok($decoded_marc_record->isa('MARC::Record'), "ARRAY record successfully decoded from result");
750     is($decoded_marc_record->as_usmarc(), $marc_record_1->as_usmarc(), "Decoded ARRAY record has same data as original record");
751 };
752
753 subtest 'Koha::SearchEngine::Elasticsearch::marc_records_to_documents () authority tests' => sub {
754
755     plan tests => 5;
756
757     t::lib::Mocks::mock_preference('marcflavour', 'MARC21');
758     t::lib::Mocks::mock_preference('ElasticsearchMARCFormat', 'ISO2709');
759
760     my $builder = t::lib::TestBuilder->new;
761     my $auth_type = $builder->build_object({ class => 'Koha::Authority::Types', value =>{
762             auth_tag_to_report => '150'
763         }
764     });
765
766     my @mappings = (
767         {
768             name => 'match',
769             type => 'string',
770             facet => 0,
771             suggestible => 0,
772             searchable => 1,
773             sort => 0,
774             marc_type => 'marc21',
775             marc_field => '150(aevxyz)',
776         },
777         {
778             name => 'match',
779             type => 'string',
780             facet => 0,
781             suggestible => 0,
782             searchable => 1,
783             sort => 0,
784             marc_type => 'marc21',
785             marc_field => '185v',
786         }
787     );
788
789     my $se = Test::MockModule->new('Koha::SearchEngine::Elasticsearch');
790     $se->mock('_foreach_mapping', sub {
791         my ($self, $sub) = @_;
792
793         foreach my $map (@mappings) {
794             $sub->(
795                 $map->{name},
796                 $map->{type},
797                 $map->{facet},
798                 $map->{suggestible},
799                 $map->{sort},
800                 $map->{searchable},
801                 $map->{marc_type},
802                 $map->{marc_field}
803             );
804         }
805     });
806
807     my $see = Koha::SearchEngine::Elasticsearch::Search->new({ index => $Koha::SearchEngine::Elasticsearch::AUTHORITIES_INDEX });
808     my $marc_record_1 = MARC::Record->new();
809     $marc_record_1->append_fields(
810         MARC::Field->new('001', '123'),
811         MARC::Field->new('007', 'ku'),
812         MARC::Field->new('020', '', '', a => '1-56619-909-3'),
813         MARC::Field->new('150', '', '', a => 'Subject', v => 'Genresubdiv', x => 'Generalsubdiv', z => 'Geosubdiv'),
814     );
815     my $marc_record_2 = MARC::Record->new();
816     $marc_record_2->append_fields(
817         MARC::Field->new('150', '', '', a => 'Subject', v => 'Genresubdiv', z => 'Geosubdiv', x => 'Generalsubdiv', e => 'wrongsubdiv' ),
818     );
819     my $marc_record_3 = MARC::Record->new();
820     $marc_record_3->append_fields(
821         MARC::Field->new('185', '', '', v => 'Formsubdiv' ),
822     );
823     my $records = [ $marc_record_1, $marc_record_2, $marc_record_3 ];
824
825     $see->get_elasticsearch_mappings(); #sort_fields will call this and use the actual db values unless we call it first
826
827     my $docs = $see->marc_records_to_documents($records);
828
829     is_deeply(
830         [ "Subject formsubdiv Genresubdiv generalsubdiv Generalsubdiv geographicsubdiv Geosubdiv" ],
831         $docs->[0]->{'match-heading'},
832         "First record match-heading should contain the correctly formatted heading"
833     );
834     is_deeply(
835         [ "Subject formsubdiv Genresubdiv geographicsubdiv Geosubdiv generalsubdiv Generalsubdiv" ],
836         $docs->[1]->{'match-heading'},
837         "Second record match-heading should contain the correctly formatted heading without wrong subfield"
838     );
839     is_deeply(
840         [ "Subject Genresubdiv Geosubdiv Generalsubdiv wrongsubdiv" ],
841         $docs->[1]->{'match'} ,
842         "Second record heading should contain the subfields with record order retained"
843     );
844     ok( !exists $docs->[2]->{'match-heading'}, "No match heading defined for subdivision record");
845     is_deeply(
846         [ "Formsubdiv" ],
847         $docs->[2]->{'match'} ,
848         "Third record heading should contain the subfield"
849     );
850
851 };
852
853 $schema->storage->txn_rollback;