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