Bug 30110: Fix concatenation during assignements
[koha.git] / t / db_dependent / OAI / Server.t
1 #!/usr/bin/perl
2
3 # Copyright Tamil s.a.r.l. 2016
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20 use Modern::Perl;
21 use Test::Deep qw( cmp_deeply re );
22 use Test::MockTime qw/set_fixed_time set_relative_time restore_time/;
23
24 use Test::More tests => 34;
25 use DateTime;
26 use File::Basename;
27 use File::Spec;
28 use Test::MockModule;
29 use Test::Warn;
30 use XML::Simple;
31 use YAML::XS;
32
33 use t::lib::Mocks;
34 use t::lib::TestBuilder;
35
36 use C4::Biblio qw( AddBiblio GetMarcBiblio ModBiblio DelBiblio );
37 use C4::Context;
38 use C4::OAI::Sets qw(AddOAISet);
39
40 use Koha::Biblio::Metadatas;
41 use Koha::Database;
42 use Koha::DateUtils qw( dt_from_string );
43
44 BEGIN {
45     use_ok('Koha::OAI::Server::DeletedRecord');
46     use_ok('Koha::OAI::Server::Description');
47     use_ok('Koha::OAI::Server::GetRecord');
48     use_ok('Koha::OAI::Server::Identify');
49     use_ok('Koha::OAI::Server::ListBase');
50     use_ok('Koha::OAI::Server::ListIdentifiers');
51     use_ok('Koha::OAI::Server::ListMetadataFormats');
52     use_ok('Koha::OAI::Server::ListRecords');
53     use_ok('Koha::OAI::Server::ListSets');
54     use_ok('Koha::OAI::Server::Record');
55     use_ok('Koha::OAI::Server::Repository');
56     use_ok('Koha::OAI::Server::ResumptionToken');
57 }
58
59 use constant NUMBER_OF_MARC_RECORDS => 10;
60
61 # Mocked CGI module in order to be able to send CGI parameters to OAI Server
62 my %param;
63 my $module = Test::MockModule->new('CGI');
64 $module->mock('Vars', sub { %param; });
65
66 my $schema = Koha::Database->schema;
67 $schema->storage->txn_begin;
68 my $dbh = C4::Context->dbh;
69
70 $dbh->do("SET time_zone='+00:00'");
71 $dbh->do('DELETE FROM issues');
72 $dbh->do('DELETE FROM biblio');
73 $dbh->do('DELETE FROM deletedbiblio');
74 $dbh->do('DELETE FROM deletedbiblioitems');
75 $dbh->do('DELETE FROM deleteditems');
76 $dbh->do('DELETE FROM oai_sets');
77
78 set_fixed_time(CORE::time());
79
80 my $base_datetime = dt_from_string(undef, undef, 'UTC');
81 my $date_added = $base_datetime->ymd . ' ' .$base_datetime->hms . 'Z';
82 my $date_to = substr($date_added, 0, 10) . 'T23:59:59Z';
83 my (@header, @marcxml, @oaidc, @marcxml_transformed);
84 my $sth = $dbh->prepare('UPDATE biblioitems     SET timestamp=? WHERE biblionumber=?');
85 my $sth2 = $dbh->prepare('UPDATE biblio_metadata SET timestamp=? WHERE biblionumber=?');
86 my $first_bn = 0;
87
88 # Add biblio records
89 foreach my $index ( 0 .. NUMBER_OF_MARC_RECORDS - 1 ) {
90     my $record = MARC::Record->new();
91     if (C4::Context->preference('marcflavour') eq 'UNIMARC') {
92         $record->append_fields( MARC::Field->new('101', '', '', 'a' => "lng" ) );
93         $record->append_fields( MARC::Field->new('200', '', '', 'a' => "Title $index" ) );
94         $record->append_fields( MARC::Field->new('952', '', '', 'a' => "Code" ) );
95     } else {
96         $record->append_fields( MARC::Field->new('008', '                                   lng' ) );
97         $record->append_fields( MARC::Field->new('245', '', '', 'a' => "Title $index" ) );
98         $record->append_fields( MARC::Field->new('952', '', '', 'a' => "Code" ) );
99     }
100     my ($biblionumber) = AddBiblio($record, '');
101     $first_bn = $biblionumber unless $first_bn;
102     my $timestamp = $base_datetime->ymd . ' ' .$base_datetime->hms;
103     $sth->execute($timestamp,$biblionumber);
104     $sth2->execute($timestamp,$biblionumber);
105     $timestamp .= 'Z';
106     $timestamp =~ s/ /T/;
107     $record = GetMarcBiblio({ biblionumber => $biblionumber });
108     my $record_transformed = $record->clone;
109     $record_transformed->delete_fields( $record_transformed->field('952'));
110     $record_transformed = XMLin($record_transformed->as_xml_record);
111     $record = XMLin($record->as_xml_record);
112     push @header, { datestamp => $timestamp, identifier => "TEST:$biblionumber" };
113     my $dc = {
114         'dc:title' => "Title $index",
115         'dc:language' => "lng",
116         'dc:type' => {},
117         'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
118         'xmlns:oai_dc' => 'http://www.openarchives.org/OAI/2.0/oai_dc/',
119         'xmlns:dc' => 'http://purl.org/dc/elements/1.1/',
120         'xsi:schemaLocation' => 'http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd',
121     };
122     if (C4::Context->preference('marcflavour') eq 'UNIMARC') {
123         $dc->{'dc:identifier'} = $biblionumber;
124     }
125     push @oaidc, {
126         header => $header[$index],
127         metadata => {
128             'oai_dc:dc' => $dc,
129         },
130     };
131     push @marcxml, {
132         header => $header[$index],
133         metadata => {
134             record => $record,
135         },
136     };
137
138     push @marcxml_transformed, {
139         header => $header[$index],
140         metadata => {
141             record => $record_transformed,
142         },
143     };
144 }
145
146 my $syspref = {
147     'LibraryName'           => 'My Library',
148     'OAI::PMH'              => 1,
149     'OAI-PMH:archiveID'     => 'TEST',
150     'OAI-PMH:ConfFile'      => '',
151     'OAI-PMH:MaxCount'      => 3,
152     'OAI-PMH:DeletedRecord' => 'persistent',
153 };
154 while ( my ($name, $value) = each %$syspref ) {
155     t::lib::Mocks::mock_preference( $name => $value );
156 }
157
158 sub test_query {
159     my ($test, $param, $expected) = @_;
160
161     %param = %$param;
162     my %full_expected = (
163         %$expected,
164         (
165             request      => 'http://localhost',
166             xmlns        => 'http://www.openarchives.org/OAI/2.0/',
167             'xmlns:xsi'  => 'http://www.w3.org/2001/XMLSchema-instance',
168             'xsi:schemaLocation' => 'http://www.openarchives.org/OAI/2.0/ http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd',
169         )
170     );
171
172     my $response;
173     {
174         my $stdout;
175         local *STDOUT;
176         open STDOUT, '>', \$stdout;
177         Koha::OAI::Server::Repository->new();
178         $response = XMLin($stdout);
179     }
180
181     delete $response->{responseDate};
182     unless (cmp_deeply($response, \%full_expected, $test)) {
183         diag
184             "PARAM:" . YAML::XS::Dump($param) .
185             "EXPECTED:" . YAML::XS::Dump(\%full_expected) .
186             "RESPONSE:" . YAML::XS::Dump($response);
187     }
188 }
189
190 test_query('ListMetadataFormats', {verb => 'ListMetadataFormats'}, {
191     ListMetadataFormats => {
192         metadataFormat => [
193             {
194                 metadataNamespace => 'http://www.openarchives.org/OAI/2.0/oai_dc/',
195                 metadataPrefix=> 'oai_dc',
196                 schema => 'http://www.openarchives.org/OAI/2.0/oai_dc.xsd',
197             },
198             {
199                 metadataNamespace => 'http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim',
200                 metadataPrefix => 'marc21',
201                 schema => 'http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd',
202             },
203             {
204                 metadataNamespace => 'http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim',
205                 metadataPrefix => 'marcxml',
206                 schema => 'http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd',
207             },
208         ],
209     },
210 });
211
212 test_query('ListIdentifiers without metadataPrefix', {verb => 'ListIdentifiers'}, {
213     error => {
214         code => 'badArgument',
215         content => "Required argument 'metadataPrefix' was undefined",
216     },
217 });
218
219 test_query('ListIdentifiers', {verb => 'ListIdentifiers', metadataPrefix => 'marcxml'}, {
220     ListIdentifiers => {
221         header => [ @header[0..2] ],
222         resumptionToken => {
223             content => re( qr{^marcxml/3////0/0/\d+$} ),
224             cursor  => 3,
225         },
226     },
227 });
228
229 test_query('ListIdentifiers', {verb => 'ListIdentifiers', metadataPrefix => 'marcxml'}, {
230     ListIdentifiers => {
231         header => [ @header[0..2] ],
232         resumptionToken => {
233             content => re( qr{^marcxml/3////0/0/\d+$} ),
234             cursor  => 3,
235         },
236     },
237 });
238
239 test_query(
240     'ListIdentifiers with resumptionToken 1',
241     { verb => 'ListIdentifiers', resumptionToken => "marcxml/3/1970-01-01T00:00:00Z/$date_to//0/0/" . ($first_bn + 3) },
242     {
243         ListIdentifiers => {
244             header => [ @header[3..5] ],
245             resumptionToken => {
246               content => re( qr{^marcxml/6/1970-01-01T00:00:00Z/$date_to//0/0/\d+$} ),
247               cursor  => 6,
248             },
249           },
250     },
251 );
252
253 test_query(
254     'ListIdentifiers with resumptionToken 2',
255     { verb => 'ListIdentifiers', resumptionToken => "marcxml/6/1970-01-01T00:00:00Z/$date_to//0/0/" . ($first_bn + 6) },
256     {
257         ListIdentifiers => {
258             header => [ @header[6..8] ],
259             resumptionToken => {
260               content => re( qr{^marcxml/9/1970-01-01T00:00:00Z/$date_to//0/0/\d+$} ),
261               cursor  => 9,
262             },
263           },
264     },
265 );
266
267 test_query(
268     'ListIdentifiers with resumptionToken 3, response without resumption',
269     { verb => 'ListIdentifiers', resumptionToken => "marcxml/9/1970-01-01T00:00:00Z/$date_to//0/0/" . ($first_bn + 9) },
270     {
271         ListIdentifiers => {
272             header => $header[9],
273           },
274     },
275 );
276
277 test_query('ListRecords marcxml without metadataPrefix', {verb => 'ListRecords'}, {
278     error => {
279         code => 'badArgument',
280         content => "Required argument 'metadataPrefix' was undefined",
281     },
282 });
283
284 test_query('ListRecords marcxml', {verb => 'ListRecords', metadataPrefix => 'marcxml'}, {
285     ListRecords => {
286         record => [ @marcxml[0..2] ],
287         resumptionToken => {
288           content => re( qr{^marcxml/3////0/0/\d+$} ),
289           cursor  => 3,
290         },
291     },
292 });
293
294 test_query(
295     'ListRecords marcxml with resumptionToken 1',
296     { verb => 'ListRecords', resumptionToken => "marcxml/3////0/0/" . ($first_bn + 3) },
297     { ListRecords => {
298         record => [ @marcxml[3..5] ],
299         resumptionToken => {
300           content => re( qr{^marcxml/6////0/0/\d+$} ),
301           cursor  => 6,
302         },
303     },
304 });
305
306 test_query(
307     'ListRecords marcxml with resumptionToken 2',
308     { verb => 'ListRecords', resumptionToken => "marcxml/6/1970-01-01T00:00:00Z/$date_to//0/0/" . ($first_bn + 6) },
309     { ListRecords => {
310         record => [ @marcxml[6..8] ],
311         resumptionToken => {
312           content => re( qr{^marcxml/9/1970-01-01T00:00:00Z/$date_to//0/0/\d+$} ),
313           cursor  => 9,
314         },
315     },
316 });
317
318 # Last record, so no resumption token
319 test_query(
320     'ListRecords marcxml with resumptionToken 3, response without resumption',
321     { verb => 'ListRecords', resumptionToken => "marcxml/9/1970-01-01T00:00:00Z/$date_to//0/0/" . ($first_bn + 9) },
322     { ListRecords => {
323         record => $marcxml[9],
324     },
325 });
326
327 test_query('ListRecords oai_dc', {verb => 'ListRecords', metadataPrefix => 'oai_dc'}, {
328     ListRecords => {
329         record => [ @oaidc[0..2] ],
330         resumptionToken => {
331           content => re( qr{^oai_dc/3////0/0/\d+$} ),
332           cursor  => 3,
333         },
334     },
335 });
336
337 test_query(
338     'ListRecords oai_dc with resumptionToken 1',
339     { verb => 'ListRecords', resumptionToken => "oai_dc/3////0/0/" . ($first_bn + 3) },
340     { ListRecords => {
341         record => [ @oaidc[3..5] ],
342         resumptionToken => {
343           content => re( qr{^oai_dc/6////0/0/\d+$} ),
344           cursor  => 6,
345         },
346     },
347 });
348
349 test_query(
350     'ListRecords oai_dc with resumptionToken 2',
351     { verb => 'ListRecords', resumptionToken => "oai_dc/6/1970-01-01T00:00:00Z/$date_to//0/0/" . ($first_bn + 6) },
352     { ListRecords => {
353         record => [ @oaidc[6..8] ],
354         resumptionToken => {
355           content => re( qr{^oai_dc/9/1970-01-01T00:00:00Z/$date_to//0/0/\d+$} ),
356           cursor  => 9,
357         },
358     },
359 });
360
361 # Last record, so no resumption token
362 test_query(
363     'ListRecords oai_dc with resumptionToken 3, response without resumption',
364     { verb => 'ListRecords', resumptionToken => "oai_dc/9/1970-01-01T00:00:00Z/$date_to//0/0/" . ($first_bn + 9) },
365     { ListRecords => {
366         record => $oaidc[9],
367     },
368 });
369
370 #  List records, but now transformed by XSLT
371 t::lib::Mocks::mock_preference("OAI-PMH:ConfFile" =>  File::Spec->rel2abs(dirname(__FILE__)) . "/oaiconf.yaml");
372 test_query('ListRecords marcxml with xsl transformation',
373     { verb => 'ListRecords', metadataPrefix => 'marcxml' },
374     { ListRecords => {
375         record => [ @marcxml_transformed[0..2] ],
376         resumptionToken => {
377             content => re( qr{^marcxml/3////0/0/\d+$} ),
378             cursor => 3,
379         }
380     },
381 });
382 t::lib::Mocks::mock_preference("OAI-PMH:ConfFile" => '');
383
384 restore_time();
385
386 subtest 'Bug 19725: OAI-PMH ListRecords and ListIdentifiers should use biblio_metadata.timestamp' => sub {
387     plan tests => 1;
388
389     # Wait 1 second to be sure no timestamp will be equal to $from defined below
390     sleep 1;
391
392     # Modify record to trigger auto update of timestamp
393     (my $biblionumber = $marcxml[0]->{header}->{identifier}) =~ s/^.*:(.*)/$1/;
394     my $record = GetMarcBiblio({biblionumber => $biblionumber});
395     $record->append_fields(MARC::Field->new(999, '', '', z => '_'));
396     ModBiblio( $record, $biblionumber );
397     my $from_dt = dt_from_string(
398         Koha::Biblio::Metadatas->find({ biblionumber => $biblionumber, format => 'marcxml', schema => 'MARC21' })->timestamp
399     );
400     my $from = $from_dt->ymd . 'T' . $from_dt->hms . 'Z';
401     $oaidc[0]->{header}->{datestamp} = $from;
402
403     test_query(
404         'ListRecords oai_dc with parameter from',
405         { verb => 'ListRecords', metadataPrefix => 'oai_dc', from => $from },
406         { ListRecords => {
407             record => $oaidc[0],
408         },
409     });
410 };
411
412 subtest 'Bug 20665: OAI-PMH Provider should reset the MySQL connection time zone' => sub {
413     plan tests => 2;
414
415     # Set time zone to SYSTEM so that it can be checked later
416     $dbh->do("SET time_zone='SYSTEM'");
417
418
419     test_query('ListIdentifiers without metadataPrefix', {verb => 'ListIdentifiers'}, {
420         error => {
421             code => 'badArgument',
422             content => "Required argument 'metadataPrefix' was undefined",
423         },
424     });
425
426     my $sth = C4::Context->dbh->prepare('SELECT @@session.time_zone');
427     $sth->execute();
428     my ( $tz ) = $sth->fetchrow();
429
430     ok ( $tz eq 'SYSTEM', 'MySQL connection time zone is SYSTEM' );
431 };
432
433
434 $schema->storage->txn_rollback;
435
436 subtest 'ListSets tests' => sub {
437
438     plan tests => 3;
439
440     t::lib::Mocks::mock_preference( 'OAI::PMH'         => 1 );
441     t::lib::Mocks::mock_preference( 'OAI-PMH:MaxCount' => 3 );
442
443     $schema->storage->txn_begin;
444
445     $dbh->do('DELETE FROM oai_sets');
446
447     # Add a bunch of sets
448     my @first_page_sets = ();
449     for my $i ( 1 .. 3 ) {
450
451         AddOAISet(
452             {   'spec' => "setSpec_$i",
453                 'name' => "setName_$i",
454             }
455         );
456         push @first_page_sets, { setSpec => "setSpec_$i", setName => "setName_$i" };
457     }
458
459     # Add more to force pagination
460     my @second_page_sets = ();
461     for my $i ( 4 .. 6 ) {
462
463         AddOAISet(
464             {   'spec' => "setSpec_$i",
465                 'name' => "setName_$i",
466             }
467         );
468         push @second_page_sets, { setSpec => "setSpec_$i", setName => "setName_$i" };
469     }
470
471     AddOAISet(
472         {   'spec' => "setSpec_7",
473             'name' => "setName_7",
474         }
475     );
476
477     test_query(
478         'ListSets',
479         { verb => 'ListSets' },
480         { ListSets => {
481             resumptionToken => {
482                 content => re( qr{^/3////1/0/4$} ),
483                 cursor => 3,
484             },
485             set => \@first_page_sets
486           }
487         }
488     );
489
490     test_query(
491         'ListSets',
492         { verb => 'ListSets', resumptionToken => '/3////1/0/4' },
493         { ListSets => {
494             resumptionToken => {
495                 content => re( qr{^/6////1/0/7$} ),
496                 cursor => 6,
497             },
498             set => \@second_page_sets
499           }
500         }
501     );
502
503     test_query(
504         'ListSets',
505         { verb => 'ListSets', resumptionToken => "/6////1/0/7" },
506         { ListSets => {
507             set => { setSpec => "setSpec_7", setName => "setName_7" }
508           }
509         }
510     );
511
512     $schema->storage->txn_rollback;
513 };
514
515 subtest 'Tests for timestamp handling' => sub {
516
517     plan tests => 28;
518
519     t::lib::Mocks::mock_preference( 'OAI::PMH'         => 1 );
520     t::lib::Mocks::mock_preference( 'OAI-PMH:MaxCount' => 3 );
521     t::lib::Mocks::mock_preference( 'OAI-PMH:ConfFile' =>  File::Spec->rel2abs(dirname(__FILE__)) . '/oaiconf_items.yaml' );
522
523     $schema->storage->txn_begin;
524
525     my $sth_metadata = $dbh->prepare('UPDATE biblio_metadata SET timestamp=? WHERE biblionumber=?');
526     my $sth_del_metadata = $dbh->prepare('UPDATE deletedbiblio_metadata SET timestamp=? WHERE biblionumber=?');
527     my $sth_item = $dbh->prepare('UPDATE items SET timestamp=? WHERE itemnumber=?');
528     my $sth_del_item = $dbh->prepare('UPDATE deleteditems SET timestamp=? WHERE itemnumber=?');
529
530     my $builder = t::lib::TestBuilder->new;
531
532     set_fixed_time(CORE::time());
533
534     my $utc_datetime = dt_from_string(undef, undef, 'UTC');
535     my $utc_timestamp = $utc_datetime->ymd . 'T' . $utc_datetime->hms . 'Z';
536     my $timestamp = dt_from_string(undef, 'sql');
537
538     # Test a bib with one item
539     my $biblio1 = $builder->build_sample_biblio;
540     Koha::Biblios->find($biblio1->biblionumber)->timestamp('1970-05-07 13:36:23')->store;
541
542     $sth_metadata->execute($timestamp, $biblio1->biblionumber);
543     my $item1 = $builder->build_sample_item(
544         {
545             biblionumber => $biblio1->biblionumber
546         }
547     );
548     $sth_item->execute($timestamp, $item1->itemnumber);
549
550     my $list_items = {
551         verb => 'ListRecords',
552         metadataPrefix => 'marc21',
553         from => $utc_timestamp
554     };
555     my $list_no_items = {
556         verb => 'ListRecords',
557         metadataPrefix => 'marcxml',
558         from => $utc_timestamp
559     };
560
561     my $get_items = {
562         verb => 'GetRecord',
563         metadataPrefix => 'marc21',
564         identifier => 'TEST:' . $biblio1->biblionumber
565     };
566     my $get_no_items = {
567         verb => 'GetRecord',
568         metadataPrefix => 'marcxml',
569         identifier => 'TEST:' . $biblio1->biblionumber
570     };
571
572     my $expected = {
573         record => {
574             header => {
575                 datestamp => $utc_timestamp,
576                 identifier => 'TEST:' . $biblio1->biblionumber
577             },
578             metadata => {
579                 record => XMLin(
580                     GetMarcBiblio({ biblionumber => $biblio1->biblionumber, embed_items => 1, opac => 1 })->as_xml_record()
581                 )
582             }
583         }
584     };
585     my $expected_no_items = {
586         record => {
587             header => {
588                 datestamp => $utc_timestamp,
589                 identifier => 'TEST:' . $biblio1->biblionumber
590             },
591             metadata => {
592                 record => XMLin(
593                     GetMarcBiblio({ biblionumber => $biblio1->biblionumber, embed_items => 0, opac => 1 })->as_xml_record()
594                 )
595             }
596         }
597     };
598
599     test_query(
600         'ListRecords - biblio with a single item',
601         $list_items,
602         { ListRecords => $expected }
603     );
604     test_query(
605         'ListRecords - biblio with a single item (items not returned)',
606         $list_no_items,
607         { ListRecords => $expected_no_items }
608     );
609     test_query(
610         'GetRecord - biblio with a single item',
611         $get_items,
612         { GetRecord => $expected }
613     );
614     test_query(
615         'GetRecord - biblio with a single item (items not returned)',
616         $get_no_items,
617         { GetRecord => $expected_no_items }
618     );
619     t::lib::Mocks::mock_preference('KohaAdminEmailAddress', 'root@localhost');
620     test_query(
621         'Identify - earliestDatestamp in the right format',
622         { verb => 'Identify' },
623         {   Identify => {
624                 adminEmail        => 'root@localhost',
625                 baseURL           => 'http://localhost',
626                 compression       => 'gzip',
627                 deletedRecord     => 'persistent',
628                 earliestDatestamp => '1970-05-07T13:36:23Z',
629                 granularity       => 'YYYY-MM-DDThh:mm:ssZ',
630                 protocolVersion   => '2.0',
631                 repositoryName    => 'My Library',
632             }
633         }
634     );
635
636     # Add an item 10 seconds later and check results
637     set_relative_time(10);
638
639     $utc_datetime = dt_from_string(undef, undef, 'UTC');
640     $utc_timestamp = $utc_datetime->ymd . 'T' . $utc_datetime->hms . 'Z';
641     $timestamp = dt_from_string(undef, 'sql');
642
643     my $item2 = $builder->build_sample_item(
644         {
645             biblionumber => $biblio1->biblionumber
646         }
647     );
648     $sth_item->execute($timestamp, $item2->itemnumber);
649
650     $expected->{record}{header}{datestamp} = $utc_timestamp;
651     $expected->{record}{metadata}{record} = XMLin(
652         GetMarcBiblio({ biblionumber => $biblio1->biblionumber, embed_items => 1, opac => 1 })->as_xml_record()
653     );
654
655     test_query(
656         'ListRecords - biblio with two items',
657         $list_items,
658         { ListRecords => $expected }
659     );
660     test_query(
661         'ListRecords - biblio with two items (items not returned)',
662         $list_no_items,
663         { ListRecords => $expected_no_items }
664     );
665     test_query(
666         'GetRecord - biblio with a two items',
667         $get_items,
668         { GetRecord => $expected }
669     );
670     test_query(
671         'GetRecord - biblio with a two items (items not returned)',
672         $get_no_items,
673         { GetRecord => $expected_no_items }
674     );
675
676     # Set biblio timestamp 10 seconds later and check results
677     set_relative_time(10);
678     $utc_datetime = dt_from_string(undef, undef, 'UTC');
679     $utc_timestamp= $utc_datetime->ymd . 'T' . $utc_datetime->hms . 'Z';
680     $timestamp = dt_from_string(undef, 'sql');
681
682     $sth_metadata->execute($timestamp, $biblio1->biblionumber);
683
684     $expected->{record}{header}{datestamp} = $utc_timestamp;
685     $expected_no_items->{record}{header}{datestamp} = $utc_timestamp;
686
687     test_query(
688         "ListRecords - biblio with timestamp higher than item's",
689         $list_items,
690         { ListRecords => $expected }
691     );
692     test_query(
693         "ListRecords - biblio with timestamp higher than item's (items not returned)",
694         $list_no_items,
695         { ListRecords => $expected_no_items }
696     );
697     test_query(
698         "GetRecord - biblio with timestamp higher than item's",
699         $get_items,
700         { GetRecord => $expected }
701     );
702     test_query(
703         "GetRecord - biblio with timestamp higher than item's (items not returned)",
704         $get_no_items,
705         { GetRecord => $expected_no_items }
706     );
707
708     # Delete an item 10 seconds later and check results
709     set_relative_time(10);
710     $utc_datetime = dt_from_string(undef, undef, 'UTC');
711     $utc_timestamp = $utc_datetime->ymd . 'T' . $utc_datetime->hms . 'Z';
712
713     $item1->safe_delete({ skip_record_index =>1 });
714     $sth_del_item->execute($timestamp, $item1->itemnumber);
715
716     $expected->{record}{header}{datestamp} = $utc_timestamp;
717     $expected->{record}{metadata}{record} = XMLin(
718         GetMarcBiblio({ biblionumber => $biblio1->biblionumber, embed_items => 1, opac => 1 })->as_xml_record()
719     );
720
721     test_query(
722         'ListRecords - biblio with existing and deleted item',
723         $list_items,
724         { ListRecords => $expected }
725     );
726     test_query(
727         'ListRecords - biblio with existing and deleted item (items not returned)',
728         $list_no_items,
729         { ListRecords => $expected_no_items }
730     );
731     test_query(
732         'GetRecord - biblio with existing and deleted item',
733         $get_items,
734         { GetRecord => $expected }
735     );
736     test_query(
737         'GetRecord - biblio with existing and deleted item (items not returned)',
738         $get_no_items,
739         { GetRecord => $expected_no_items }
740     );
741
742     # Delete also the second item and verify results
743     $item2->safe_delete({ skip_record_index =>1 });
744     $sth_del_item->execute($timestamp, $item2->itemnumber);
745
746     $expected->{record}{metadata}{record} = XMLin(
747         GetMarcBiblio({ biblionumber => $biblio1->biblionumber, embed_items => 1, opac => 1 })->as_xml_record()
748     );
749
750     test_query(
751         'ListRecords - biblio with two deleted items',
752         $list_items,
753         { ListRecords => $expected }
754     );
755     test_query(
756         'ListRecords - biblio with two deleted items (items not returned)',
757         $list_no_items,
758         { ListRecords => $expected_no_items }
759     );
760     test_query(
761         'GetRecord - biblio with two deleted items',
762         $get_items,
763         { GetRecord => $expected }
764     );
765     test_query(
766         'GetRecord - biblio with two deleted items (items not returned)',
767         $get_no_items,
768         { GetRecord => $expected_no_items }
769     );
770
771     # Delete the biblio 10 seconds later and check results
772     set_relative_time(10);
773     $utc_datetime = dt_from_string(undef, undef, 'UTC');
774     $utc_timestamp = $utc_datetime->ymd . 'T' . $utc_datetime->hms . 'Z';
775     $timestamp = dt_from_string(undef, 'sql');
776
777     is(undef, DelBiblio($biblio1->biblionumber, { skip_record_index =>1 }), 'Biblio deleted');
778     $sth_del_metadata->execute($timestamp, $biblio1->biblionumber);
779
780     my $expected_header = {
781         record => {
782             header => {
783                 datestamp => $utc_timestamp,
784                 identifier => 'TEST:' . $biblio1->biblionumber,
785                 status => 'deleted'
786             }
787         }
788     };
789
790     test_query(
791         'ListRecords - deleted biblio with two deleted items',
792         $list_items,
793         { ListRecords => $expected_header }
794     );
795     test_query(
796         'ListRecords - deleted biblio with two deleted items (items not returned)',
797         $list_no_items,
798         { ListRecords => $expected_header }
799     );
800     test_query(
801         'GetRecord - deleted biblio with two deleted items',
802         $get_items,
803         { GetRecord => $expected_header }
804     );
805     test_query(
806         'GetRecord - deleted biblio with two deleted items (items not returned)',
807         $get_no_items,
808         { GetRecord => $expected_header }
809     );
810
811     # Add a second biblio 10 seconds later and check that both are returned properly
812     set_relative_time(10);
813     $utc_datetime = dt_from_string(undef, undef, 'UTC');
814     $utc_timestamp = $utc_datetime->ymd . 'T' . $utc_datetime->hms . 'Z';
815     $timestamp = dt_from_string(undef, 'sql');
816
817     my $biblio2 = $builder->build_sample_biblio();
818     $sth_metadata->execute($timestamp, $biblio2->biblionumber);
819
820     my $expected2 = {
821         record => [
822             $expected_header->{record},
823             {
824                 header => {
825                     datestamp => $utc_timestamp,
826                     identifier => 'TEST:' . $biblio2->biblionumber
827                 },
828                 metadata => {
829                     record => XMLin(
830                         GetMarcBiblio({ biblionumber => $biblio2->biblionumber, embed_items => 1, opac => 1 })->as_xml_record()
831                     )
832                 }
833             }
834         ]
835     };
836     my $expected2_no_items = {
837         record => [
838             $expected_header->{record},
839             {
840                 header => {
841                     datestamp => $utc_timestamp,
842                     identifier => 'TEST:' . $biblio2->biblionumber
843                 },
844                 metadata => {
845                     record => XMLin(
846                         GetMarcBiblio({ biblionumber => $biblio2->biblionumber, embed_items => 0, opac => 1 })->as_xml_record()
847                     )
848                 }
849             }
850         ]
851     };
852
853     test_query(
854         'ListRecords - deleted biblio and normal biblio',
855         $list_items,
856         { ListRecords => $expected2 }
857     );
858     test_query(
859         'ListRecords - deleted biblio and normal biblio (items not returned)',
860         $list_no_items,
861         { ListRecords => $expected2_no_items }
862     );
863
864     restore_time();
865
866     $schema->storage->txn_rollback;
867 };
868
869 subtest 'ListSets() tests' => sub {
870
871     plan tests => 2;
872
873     $schema->storage->txn_begin;
874
875     # initial cleanup
876     $schema->resultset('OaiSet')->delete;
877
878     test_query(
879         'ListSets - no sets should return a noSetHierarchy exception',
880         { verb => 'ListSets' },
881         {
882             error => {
883                 code    => 'noSetHierarchy',
884                 content => 'There are no OAI sets defined',
885             }
886         }
887     );
888
889     # Add a couple sets
890     AddOAISet({ spec => 'set_1', name => 'Set 1' });
891     AddOAISet({ spec => 'set_2', name => 'Set 2' });
892
893     test_query(
894         'ListSets - no sets should return a noSetHierarchy exception',
895         { verb => 'ListSets' },
896         {
897             ListSets => {
898                 set => [
899                     { setSpec => 'set_1', setName => 'Set 1' },
900                     { setSpec => 'set_2', setName => 'Set 2' },
901                 ]
902             }
903         }
904     );
905
906     $schema->storage->txn_rollback;
907 };