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