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