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