Bug 17600: Standardize our EXPORT_OK
[srvgit] / 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 restore_time/;
23
24 use Test::More tests => 31;
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
35 use C4::Biblio qw( AddBiblio GetMarcBiblio ModBiblio );
36 use C4::Context;
37
38 use Koha::Biblio::Metadatas;
39 use Koha::Database;
40 use Koha::DateUtils;
41
42 BEGIN {
43     use_ok('Koha::OAI::Server::DeletedRecord');
44     use_ok('Koha::OAI::Server::Description');
45     use_ok('Koha::OAI::Server::GetRecord');
46     use_ok('Koha::OAI::Server::Identify');
47     use_ok('Koha::OAI::Server::ListBase');
48     use_ok('Koha::OAI::Server::ListIdentifiers');
49     use_ok('Koha::OAI::Server::ListMetadataFormats');
50     use_ok('Koha::OAI::Server::ListRecords');
51     use_ok('Koha::OAI::Server::ListSets');
52     use_ok('Koha::OAI::Server::Record');
53     use_ok('Koha::OAI::Server::Repository');
54     use_ok('Koha::OAI::Server::ResumptionToken');
55 }
56
57 use constant NUMBER_OF_MARC_RECORDS => 10;
58
59 # Mocked CGI module in order to be able to send CGI parameters to OAI Server
60 my %param;
61 my $module = Test::MockModule->new('CGI');
62 $module->mock('Vars', sub { %param; });
63
64 my $schema = Koha::Database->schema;
65 $schema->storage->txn_begin;
66 my $dbh = C4::Context->dbh;
67
68 $dbh->do("SET time_zone='+00:00'");
69 $dbh->do('DELETE FROM issues');
70 $dbh->do('DELETE FROM biblio');
71 $dbh->do('DELETE FROM deletedbiblio');
72 $dbh->do('DELETE FROM deletedbiblioitems');
73 $dbh->do('DELETE FROM deleteditems');
74 $dbh->do('DELETE FROM oai_sets');
75
76 set_fixed_time(CORE::time());
77
78 my $base_datetime = dt_from_string(undef, undef, 'UTC');
79 my $date_added = $base_datetime->ymd . ' ' .$base_datetime->hms . 'Z';
80 my $date_to = substr($date_added, 0, 10) . 'T23:59:59Z';
81 my (@header, @marcxml, @oaidc, @marcxml_transformed);
82 my $sth = $dbh->prepare('UPDATE biblioitems     SET timestamp=? WHERE biblionumber=?');
83 my $sth2 = $dbh->prepare('UPDATE biblio_metadata SET timestamp=? WHERE biblionumber=?');
84 my $first_bn = 0;
85
86 # Add biblio records
87 foreach my $index ( 0 .. NUMBER_OF_MARC_RECORDS - 1 ) {
88     my $record = MARC::Record->new();
89     if (C4::Context->preference('marcflavour') eq 'UNIMARC') {
90         $record->append_fields( MARC::Field->new('101', '', '', 'a' => "lng" ) );
91         $record->append_fields( MARC::Field->new('200', '', '', 'a' => "Title $index" ) );
92         $record->append_fields( MARC::Field->new('952', '', '', 'a' => "Code" ) );
93     } else {
94         $record->append_fields( MARC::Field->new('008', '                                   lng' ) );
95         $record->append_fields( MARC::Field->new('245', '', '', 'a' => "Title $index" ) );
96         $record->append_fields( MARC::Field->new('952', '', '', 'a' => "Code" ) );
97     }
98     my ($biblionumber) = AddBiblio($record, '');
99     $first_bn = $biblionumber unless $first_bn;
100     my $timestamp = $base_datetime->ymd . ' ' .$base_datetime->hms;
101     $sth->execute($timestamp,$biblionumber);
102     $sth2->execute($timestamp,$biblionumber);
103     $timestamp .= 'Z';
104     $timestamp =~ s/ /T/;
105     $record = GetMarcBiblio({ biblionumber => $biblionumber });
106     my $record_transformed = $record->clone;
107     $record_transformed->delete_fields( $record_transformed->field('952'));
108     $record_transformed = XMLin($record_transformed->as_xml_record);
109     $record = XMLin($record->as_xml_record);
110     push @header, { datestamp => $timestamp, identifier => "TEST:$biblionumber" };
111     my $dc = {
112         'dc:title' => "Title $index",
113         'dc:language' => "lng",
114         'dc:type' => {},
115         'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
116         'xmlns:oai_dc' => 'http://www.openarchives.org/OAI/2.0/oai_dc/',
117         'xmlns:dc' => 'http://purl.org/dc/elements/1.1/',
118         'xsi:schemaLocation' => 'http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd',
119     };
120     if (C4::Context->preference('marcflavour') eq 'UNIMARC') {
121         $dc->{'dc:identifier'} = $biblionumber;
122     }
123     push @oaidc, {
124         header => $header[$index],
125         metadata => {
126             'oai_dc:dc' => $dc,
127         },
128     };
129     push @marcxml, {
130         header => $header[$index],
131         metadata => {
132             record => $record,
133         },
134     };
135
136     push @marcxml_transformed, {
137         header => $header[$index],
138         metadata => {
139             record => $record_transformed,
140         },
141     };
142 }
143
144 my $syspref = {
145     'LibraryName'           => 'My Library',
146     'OAI::PMH'              => 1,
147     'OAI-PMH:archiveID'     => 'TEST',
148     'OAI-PMH:ConfFile'      => '',
149     'OAI-PMH:MaxCount'      => 3,
150     'OAI-PMH:DeletedRecord' => 'persistent',
151 };
152 while ( my ($name, $value) = each %$syspref ) {
153     t::lib::Mocks::mock_preference( $name => $value );
154 }
155
156 sub test_query {
157     my ($test, $param, $expected) = @_;
158
159     %param = %$param;
160     my %full_expected = (
161         %$expected,
162         (
163             request      => 'http://localhost',
164             xmlns        => 'http://www.openarchives.org/OAI/2.0/',
165             'xmlns:xsi'  => 'http://www.w3.org/2001/XMLSchema-instance',
166             'xsi:schemaLocation' => 'http://www.openarchives.org/OAI/2.0/ http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd',
167         )
168     );
169
170     my $response;
171     {
172         my $stdout;
173         local *STDOUT;
174         open STDOUT, '>', \$stdout;
175         Koha::OAI::Server::Repository->new();
176         $response = XMLin($stdout);
177     }
178
179     delete $response->{responseDate};
180     unless (cmp_deeply($response, \%full_expected, $test)) {
181         diag
182             "PARAM:" . YAML::XS::Dump($param) .
183             "EXPECTED:" . YAML::XS::Dump(\%full_expected) .
184             "RESPONSE:" . YAML::XS::Dump($response);
185     }
186 }
187
188 test_query('ListMetadataFormats', {verb => 'ListMetadataFormats'}, {
189     ListMetadataFormats => {
190         metadataFormat => [
191             {
192                 metadataNamespace => 'http://www.openarchives.org/OAI/2.0/oai_dc/',
193                 metadataPrefix=> 'oai_dc',
194                 schema => 'http://www.openarchives.org/OAI/2.0/oai_dc.xsd',
195             },
196             {
197                 metadataNamespace => 'http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim',
198                 metadataPrefix => 'marc21',
199                 schema => 'http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd',
200             },
201             {
202                 metadataNamespace => 'http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim',
203                 metadataPrefix => 'marcxml',
204                 schema => 'http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd',
205             },
206         ],
207     },
208 });
209
210 test_query('ListIdentifiers without metadataPrefix', {verb => 'ListIdentifiers'}, {
211     error => {
212         code => 'badArgument',
213         content => "Required argument 'metadataPrefix' was undefined",
214     },
215 });
216
217 test_query('ListIdentifiers', {verb => 'ListIdentifiers', metadataPrefix => 'marcxml'}, {
218     ListIdentifiers => {
219         header => [ @header[0..2] ],
220         resumptionToken => {
221             content => re( qr{^marcxml/3////0/0/\d+$} ),
222             cursor  => 3,
223         },
224     },
225 });
226
227 test_query('ListIdentifiers', {verb => 'ListIdentifiers', metadataPrefix => 'marcxml'}, {
228     ListIdentifiers => {
229         header => [ @header[0..2] ],
230         resumptionToken => {
231             content => re( qr{^marcxml/3////0/0/\d+$} ),
232             cursor  => 3,
233         },
234     },
235 });
236
237 test_query(
238     'ListIdentifiers with resumptionToken 1',
239     { verb => 'ListIdentifiers', resumptionToken => "marcxml/3/1970-01-01T00:00:00Z/$date_to//0/0/" . ($first_bn + 3) },
240     {
241         ListIdentifiers => {
242             header => [ @header[3..5] ],
243             resumptionToken => {
244               content => re( qr{^marcxml/6/1970-01-01T00:00:00Z/$date_to//0/0/\d+$} ),
245               cursor  => 6,
246             },
247           },
248     },
249 );
250
251 test_query(
252     'ListIdentifiers with resumptionToken 2',
253     { verb => 'ListIdentifiers', resumptionToken => "marcxml/6/1970-01-01T00:00:00Z/$date_to//0/0/" . ($first_bn + 6) },
254     {
255         ListIdentifiers => {
256             header => [ @header[6..8] ],
257             resumptionToken => {
258               content => re( qr{^marcxml/9/1970-01-01T00:00:00Z/$date_to//0/0/\d+$} ),
259               cursor  => 9,
260             },
261           },
262     },
263 );
264
265 test_query(
266     'ListIdentifiers with resumptionToken 3, response without resumption',
267     { verb => 'ListIdentifiers', resumptionToken => "marcxml/9/1970-01-01T00:00:00Z/$date_to//0/0/" . ($first_bn + 9) },
268     {
269         ListIdentifiers => {
270             header => $header[9],
271           },
272     },
273 );
274
275 test_query('ListRecords marcxml without metadataPrefix', {verb => 'ListRecords'}, {
276     error => {
277         code => 'badArgument',
278         content => "Required argument 'metadataPrefix' was undefined",
279     },
280 });
281
282 test_query('ListRecords marcxml', {verb => 'ListRecords', metadataPrefix => 'marcxml'}, {
283     ListRecords => {
284         record => [ @marcxml[0..2] ],
285         resumptionToken => {
286           content => re( qr{^marcxml/3////0/0/\d+$} ),
287           cursor  => 3,
288         },
289     },
290 });
291
292 test_query(
293     'ListRecords marcxml with resumptionToken 1',
294     { verb => 'ListRecords', resumptionToken => "marcxml/3////0/0/" . ($first_bn + 3) },
295     { ListRecords => {
296         record => [ @marcxml[3..5] ],
297         resumptionToken => {
298           content => re( qr{^marcxml/6////0/0/\d+$} ),
299           cursor  => 6,
300         },
301     },
302 });
303
304 test_query(
305     'ListRecords marcxml with resumptionToken 2',
306     { verb => 'ListRecords', resumptionToken => "marcxml/6/1970-01-01T00:00:00Z/$date_to//0/0/" . ($first_bn + 6) },
307     { ListRecords => {
308         record => [ @marcxml[6..8] ],
309         resumptionToken => {
310           content => re( qr{^marcxml/9/1970-01-01T00:00:00Z/$date_to//0/0/\d+$} ),
311           cursor  => 9,
312         },
313     },
314 });
315
316 # Last record, so no resumption token
317 test_query(
318     'ListRecords marcxml with resumptionToken 3, response without resumption',
319     { verb => 'ListRecords', resumptionToken => "marcxml/9/1970-01-01T00:00:00Z/$date_to//0/0/" . ($first_bn + 9) },
320     { ListRecords => {
321         record => $marcxml[9],
322     },
323 });
324
325 test_query('ListRecords oai_dc', {verb => 'ListRecords', metadataPrefix => 'oai_dc'}, {
326     ListRecords => {
327         record => [ @oaidc[0..2] ],
328         resumptionToken => {
329           content => re( qr{^oai_dc/3////0/0/\d+$} ),
330           cursor  => 3,
331         },
332     },
333 });
334
335 test_query(
336     'ListRecords oai_dc with resumptionToken 1',
337     { verb => 'ListRecords', resumptionToken => "oai_dc/3////0/0/" . ($first_bn + 3) },
338     { ListRecords => {
339         record => [ @oaidc[3..5] ],
340         resumptionToken => {
341           content => re( qr{^oai_dc/6////0/0/\d+$} ),
342           cursor  => 6,
343         },
344     },
345 });
346
347 test_query(
348     'ListRecords oai_dc with resumptionToken 2',
349     { verb => 'ListRecords', resumptionToken => "oai_dc/6/1970-01-01T00:00:00Z/$date_to//0/0/" . ($first_bn + 6) },
350     { ListRecords => {
351         record => [ @oaidc[6..8] ],
352         resumptionToken => {
353           content => re( qr{^oai_dc/9/1970-01-01T00:00:00Z/$date_to//0/0/\d+$} ),
354           cursor  => 9,
355         },
356     },
357 });
358
359 # Last record, so no resumption token
360 test_query(
361     'ListRecords oai_dc with resumptionToken 3, response without resumption',
362     { verb => 'ListRecords', resumptionToken => "oai_dc/9/1970-01-01T00:00:00Z/$date_to//0/0/" . ($first_bn + 9) },
363     { ListRecords => {
364         record => $oaidc[9],
365     },
366 });
367
368 #  List records, but now transformed by XSLT
369 t::lib::Mocks::mock_preference("OAI-PMH:ConfFile" =>  File::Spec->rel2abs(dirname(__FILE__)) . "/oaiconf.yaml");
370 test_query('ListRecords marcxml with xsl transformation',
371     { verb => 'ListRecords', metadataPrefix => 'marcxml' },
372     { ListRecords => {
373         record => [ @marcxml_transformed[0..2] ],
374         resumptionToken => {
375             content => re( qr{^marcxml/3////0/0/\d+$} ),
376             cursor => 3,
377         }
378     },
379 });
380 t::lib::Mocks::mock_preference("OAI-PMH:ConfFile" => '');
381
382 restore_time();
383
384 subtest 'Bug 19725: OAI-PMH ListRecords and ListIdentifiers should use biblio_metadata.timestamp' => sub {
385     plan tests => 1;
386
387     # Wait 1 second to be sure no timestamp will be equal to $from defined below
388     sleep 1;
389
390     # Modify record to trigger auto update of timestamp
391     (my $biblionumber = $marcxml[0]->{header}->{identifier}) =~ s/^.*:(.*)/$1/;
392     my $record = GetMarcBiblio({biblionumber => $biblionumber});
393     $record->append_fields(MARC::Field->new(999, '', '', z => '_'));
394     ModBiblio( $record, $biblionumber );
395     my $from_dt = dt_from_string(
396         Koha::Biblio::Metadatas->find({ biblionumber => $biblionumber, format => 'marcxml', schema => 'MARC21' })->timestamp
397     );
398     my $from = $from_dt->ymd . 'T' . $from_dt->hms . 'Z';
399     $oaidc[0]->{header}->{datestamp} = $from;
400
401     test_query(
402         'ListRecords oai_dc with parameter from',
403         { verb => 'ListRecords', metadataPrefix => 'oai_dc', from => $from },
404         { ListRecords => {
405             record => $oaidc[0],
406         },
407     });
408 };
409
410 subtest 'Bug 20665: OAI-PMH Provider should reset the MySQL connection time zone' => sub {
411     plan tests => 2;
412
413     # Set time zone to SYSTEM so that it can be checked later
414     $dbh->do("SET time_zone='SYSTEM'");
415
416
417     test_query('ListIdentifiers without metadataPrefix', {verb => 'ListIdentifiers'}, {
418         error => {
419             code => 'badArgument',
420             content => "Required argument 'metadataPrefix' was undefined",
421         },
422     });
423
424     my $sth = C4::Context->dbh->prepare('SELECT @@session.time_zone');
425     $sth->execute();
426     my ( $tz ) = $sth->fetchrow();
427
428     ok ( $tz eq 'SYSTEM', 'MySQL connection time zone is SYSTEM' );
429 };
430
431
432 $schema->storage->txn_rollback;