Bug 29135: Fix handling of deleted items in OAI-PMH provider
[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 => 33;
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;
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;
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 => 27;
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     $sth_metadata->execute($timestamp, $biblio1->biblionumber);
541     my $item1 = $builder->build_sample_item(
542         {
543             biblionumber => $biblio1->biblionumber
544         }
545     );
546     $sth_item->execute($timestamp, $item1->itemnumber);
547
548     my $list_items = {
549         verb => 'ListRecords',
550         metadataPrefix => 'marc21',
551         from => $utc_timestamp
552     };
553     my $list_no_items = {
554         verb => 'ListRecords',
555         metadataPrefix => 'marcxml',
556         from => $utc_timestamp
557     };
558
559     my $get_items = {
560         verb => 'GetRecord',
561         metadataPrefix => 'marc21',
562         identifier => 'TEST:' . $biblio1->biblionumber
563     };
564     my $get_no_items = {
565         verb => 'GetRecord',
566         metadataPrefix => 'marcxml',
567         identifier => 'TEST:' . $biblio1->biblionumber
568     };
569
570     my $expected = {
571         record => {
572             header => {
573                 datestamp => $utc_timestamp,
574                 identifier => 'TEST:' . $biblio1->biblionumber
575             },
576             metadata => {
577                 record => XMLin(
578                     GetMarcBiblio({ biblionumber => $biblio1->biblionumber, embed_items => 1, opac => 1 })->as_xml_record()
579                 )
580             }
581         }
582     };
583     my $expected_no_items = {
584         record => {
585             header => {
586                 datestamp => $utc_timestamp,
587                 identifier => 'TEST:' . $biblio1->biblionumber
588             },
589             metadata => {
590                 record => XMLin(
591                     GetMarcBiblio({ biblionumber => $biblio1->biblionumber, embed_items => 0, opac => 1 })->as_xml_record()
592                 )
593             }
594         }
595     };
596
597     test_query(
598         'ListRecords - biblio with a single item',
599         $list_items,
600         { ListRecords => $expected }
601     );
602     test_query(
603         'ListRecords - biblio with a single item (items not returned)',
604         $list_no_items,
605         { ListRecords => $expected_no_items }
606     );
607     test_query(
608         'GetRecord - biblio with a single item',
609         $get_items,
610         { GetRecord => $expected }
611     );
612     test_query(
613         'GetRecord - biblio with a single item (items not returned)',
614         $get_no_items,
615         { GetRecord => $expected_no_items }
616     );
617
618     # Add an item 10 seconds later and check results
619     set_relative_time(10);
620
621     $utc_datetime = dt_from_string(undef, undef, 'UTC');
622     $utc_timestamp = $utc_datetime->ymd . 'T' . $utc_datetime->hms . 'Z';
623     $timestamp = dt_from_string(undef, 'sql');
624
625     my $item2 = $builder->build_sample_item(
626         {
627             biblionumber => $biblio1->biblionumber
628         }
629     );
630     $sth_item->execute($timestamp, $item2->itemnumber);
631
632     $expected->{record}{header}{datestamp} = $utc_timestamp;
633     $expected->{record}{metadata}{record} = XMLin(
634         GetMarcBiblio({ biblionumber => $biblio1->biblionumber, embed_items => 1, opac => 1 })->as_xml_record()
635     );
636
637     test_query(
638         'ListRecords - biblio with two items',
639         $list_items,
640         { ListRecords => $expected }
641     );
642     test_query(
643         'ListRecords - biblio with two items (items not returned)',
644         $list_no_items,
645         { ListRecords => $expected_no_items }
646     );
647     test_query(
648         'GetRecord - biblio with a two items',
649         $get_items,
650         { GetRecord => $expected }
651     );
652     test_query(
653         'GetRecord - biblio with a two items (items not returned)',
654         $get_no_items,
655         { GetRecord => $expected_no_items }
656     );
657
658     # Set biblio timestamp 10 seconds later and check results
659     set_relative_time(10);
660     $utc_datetime = dt_from_string(undef, undef, 'UTC');
661     $utc_timestamp= $utc_datetime->ymd . 'T' . $utc_datetime->hms . 'Z';
662     $timestamp = dt_from_string(undef, 'sql');
663
664     $sth_metadata->execute($timestamp, $biblio1->biblionumber);
665
666     $expected->{record}{header}{datestamp} = $utc_timestamp;
667     $expected_no_items->{record}{header}{datestamp} = $utc_timestamp;
668
669     test_query(
670         "ListRecords - biblio with timestamp higher than item's",
671         $list_items,
672         { ListRecords => $expected }
673     );
674     test_query(
675         "ListRecords - biblio with timestamp higher than item's (items not returned)",
676         $list_no_items,
677         { ListRecords => $expected_no_items }
678     );
679     test_query(
680         "GetRecord - biblio with timestamp higher than item's",
681         $get_items,
682         { GetRecord => $expected }
683     );
684     test_query(
685         "GetRecord - biblio with timestamp higher than item's (items not returned)",
686         $get_no_items,
687         { GetRecord => $expected_no_items }
688     );
689
690     # Delete an item 10 seconds later and check results
691     set_relative_time(10);
692     $utc_datetime = dt_from_string(undef, undef, 'UTC');
693     $utc_timestamp = $utc_datetime->ymd . 'T' . $utc_datetime->hms . 'Z';
694
695     $item1->safe_delete({ skip_record_index =>1 });
696     $sth_del_item->execute($timestamp, $item1->itemnumber);
697
698     $expected->{record}{header}{datestamp} = $utc_timestamp;
699     $expected->{record}{metadata}{record} = XMLin(
700         GetMarcBiblio({ biblionumber => $biblio1->biblionumber, embed_items => 1, opac => 1 })->as_xml_record()
701     );
702
703     test_query(
704         'ListRecords - biblio with existing and deleted item',
705         $list_items,
706         { ListRecords => $expected }
707     );
708     test_query(
709         'ListRecords - biblio with existing and deleted item (items not returned)',
710         $list_no_items,
711         { ListRecords => $expected_no_items }
712     );
713     test_query(
714         'GetRecord - biblio with existing and deleted item',
715         $get_items,
716         { GetRecord => $expected }
717     );
718     test_query(
719         'GetRecord - biblio with existing and deleted item (items not returned)',
720         $get_no_items,
721         { GetRecord => $expected_no_items }
722     );
723
724     # Delete also the second item and verify results
725     $item2->safe_delete({ skip_record_index =>1 });
726     $sth_del_item->execute($timestamp, $item2->itemnumber);
727
728     $expected->{record}{metadata}{record} = XMLin(
729         GetMarcBiblio({ biblionumber => $biblio1->biblionumber, embed_items => 1, opac => 1 })->as_xml_record()
730     );
731
732     test_query(
733         'ListRecords - biblio with two deleted items',
734         $list_items,
735         { ListRecords => $expected }
736     );
737     test_query(
738         'ListRecords - biblio with two deleted items (items not returned)',
739         $list_no_items,
740         { ListRecords => $expected_no_items }
741     );
742     test_query(
743         'GetRecord - biblio with two deleted items',
744         $get_items,
745         { GetRecord => $expected }
746     );
747     test_query(
748         'GetRecord - biblio with two deleted items (items not returned)',
749         $get_no_items,
750         { GetRecord => $expected_no_items }
751     );
752
753     # Delete the biblio 10 seconds later and check results
754     set_relative_time(10);
755     $utc_datetime = dt_from_string(undef, undef, 'UTC');
756     $utc_timestamp = $utc_datetime->ymd . 'T' . $utc_datetime->hms . 'Z';
757     $timestamp = dt_from_string(undef, 'sql');
758
759     is(undef, DelBiblio($biblio1->biblionumber, { skip_record_index =>1 }), 'Biblio deleted');
760     $sth_del_metadata->execute($timestamp, $biblio1->biblionumber);
761
762     my $expected_header = {
763         record => {
764             header => {
765                 datestamp => $utc_timestamp,
766                 identifier => 'TEST:' . $biblio1->biblionumber,
767                 status => 'deleted'
768             }
769         }
770     };
771
772     test_query(
773         'ListRecords - deleted biblio with two deleted items',
774         $list_items,
775         { ListRecords => $expected_header }
776     );
777     test_query(
778         'ListRecords - deleted biblio with two deleted items (items not returned)',
779         $list_no_items,
780         { ListRecords => $expected_header }
781     );
782     test_query(
783         'GetRecord - deleted biblio with two deleted items',
784         $get_items,
785         { GetRecord => $expected_header }
786     );
787     test_query(
788         'GetRecord - deleted biblio with two deleted items (items not returned)',
789         $get_no_items,
790         { GetRecord => $expected_header }
791     );
792
793     # Add a second biblio 10 seconds later and check that both are returned properly
794     set_relative_time(10);
795     $utc_datetime = dt_from_string(undef, undef, 'UTC');
796     $utc_timestamp = $utc_datetime->ymd . 'T' . $utc_datetime->hms . 'Z';
797     $timestamp = dt_from_string(undef, 'sql');
798
799     my $biblio2 = $builder->build_sample_biblio();
800     $sth_metadata->execute($timestamp, $biblio2->biblionumber);
801
802     my $expected2 = {
803         record => [
804             $expected_header->{record},
805             {
806                 header => {
807                     datestamp => $utc_timestamp,
808                     identifier => 'TEST:' . $biblio2->biblionumber
809                 },
810                 metadata => {
811                     record => XMLin(
812                         GetMarcBiblio({ biblionumber => $biblio2->biblionumber, embed_items => 1, opac => 1 })->as_xml_record()
813                     )
814                 }
815             }
816         ]
817     };
818     my $expected2_no_items = {
819         record => [
820             $expected_header->{record},
821             {
822                 header => {
823                     datestamp => $utc_timestamp,
824                     identifier => 'TEST:' . $biblio2->biblionumber
825                 },
826                 metadata => {
827                     record => XMLin(
828                         GetMarcBiblio({ biblionumber => $biblio2->biblionumber, embed_items => 0, opac => 1 })->as_xml_record()
829                     )
830                 }
831             }
832         ]
833     };
834
835     test_query(
836         'ListRecords - deleted biblio and normal biblio',
837         $list_items,
838         { ListRecords => $expected2 }
839     );
840     test_query(
841         'ListRecords - deleted biblio and normal biblio (items not returned)',
842         $list_no_items,
843         { ListRecords => $expected2_no_items }
844     );
845
846     restore_time();
847
848     $schema->storage->txn_rollback;
849 };