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