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