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