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