Koha/t/db_dependent/OAI/Server.t
Ere Maijala 155c3f1824 Bug 29135: Fix handling of deleted items in OAI-PMH provider
When the OAI-PMH provider was configured to include item information
in the returned records, deleting an item would cause the record to
be reported as deleted. The provider also did some useless checks when
determining the timestamp of a deleted record. These checks only had
a performance impact.

To test:
1) Create /var/lib/koha/kohadev/OAI.yaml with:

format:
  marcxml:
    metadataPrefix: marcxml
    metadataNamespace: http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim
    schema: http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd
    include_items: 1

2) Set preference OAI-PMH to Enable
3) Set preference OAI-PMH:ConfFile to /var/lib/koha/kohadev/OAI.yaml
4) Delete an item from a record
5) View the page: http://localhost:8080/cgi-bin/koha/oai.pl?verb=ListRecords&metadataPrefix=marcxml
6) Note the record is now listed as deleted
7) Run prove -v t/db_dependent/OAI/

Sponsored-by: The National Library of Finland

Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
2021-10-12 10:14:29 +02:00

849 lines
27 KiB
Perl
Executable file

#!/usr/bin/perl
# Copyright Tamil s.a.r.l. 2016
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Koha is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <http://www.gnu.org/licenses>.
use Modern::Perl;
use Test::Deep qw( cmp_deeply re );
use Test::MockTime qw/set_fixed_time set_relative_time restore_time/;
use Test::More tests => 33;
use DateTime;
use File::Basename;
use File::Spec;
use Test::MockModule;
use Test::Warn;
use XML::Simple;
use YAML::XS;
use t::lib::Mocks;
use t::lib::TestBuilder;
use C4::Biblio qw( AddBiblio GetMarcBiblio ModBiblio DelBiblio );
use C4::Context;
use C4::OAI::Sets qw(AddOAISet);
use Koha::Biblio::Metadatas;
use Koha::Database;
use Koha::DateUtils qw( dt_from_string );
BEGIN {
use_ok('Koha::OAI::Server::DeletedRecord');
use_ok('Koha::OAI::Server::Description');
use_ok('Koha::OAI::Server::GetRecord');
use_ok('Koha::OAI::Server::Identify');
use_ok('Koha::OAI::Server::ListBase');
use_ok('Koha::OAI::Server::ListIdentifiers');
use_ok('Koha::OAI::Server::ListMetadataFormats');
use_ok('Koha::OAI::Server::ListRecords');
use_ok('Koha::OAI::Server::ListSets');
use_ok('Koha::OAI::Server::Record');
use_ok('Koha::OAI::Server::Repository');
use_ok('Koha::OAI::Server::ResumptionToken');
}
use constant NUMBER_OF_MARC_RECORDS => 10;
# Mocked CGI module in order to be able to send CGI parameters to OAI Server
my %param;
my $module = Test::MockModule->new('CGI');
$module->mock('Vars', sub { %param; });
my $schema = Koha::Database->schema;
$schema->storage->txn_begin;
my $dbh = C4::Context->dbh;
$dbh->do("SET time_zone='+00:00'");
$dbh->do('DELETE FROM issues');
$dbh->do('DELETE FROM biblio');
$dbh->do('DELETE FROM deletedbiblio');
$dbh->do('DELETE FROM deletedbiblioitems');
$dbh->do('DELETE FROM deleteditems');
$dbh->do('DELETE FROM oai_sets');
set_fixed_time(CORE::time());
my $base_datetime = dt_from_string(undef, undef, 'UTC');
my $date_added = $base_datetime->ymd . ' ' .$base_datetime->hms . 'Z';
my $date_to = substr($date_added, 0, 10) . 'T23:59:59Z';
my (@header, @marcxml, @oaidc, @marcxml_transformed);
my $sth = $dbh->prepare('UPDATE biblioitems SET timestamp=? WHERE biblionumber=?');
my $sth2 = $dbh->prepare('UPDATE biblio_metadata SET timestamp=? WHERE biblionumber=?');
my $first_bn = 0;
# Add biblio records
foreach my $index ( 0 .. NUMBER_OF_MARC_RECORDS - 1 ) {
my $record = MARC::Record->new();
if (C4::Context->preference('marcflavour') eq 'UNIMARC') {
$record->append_fields( MARC::Field->new('101', '', '', 'a' => "lng" ) );
$record->append_fields( MARC::Field->new('200', '', '', 'a' => "Title $index" ) );
$record->append_fields( MARC::Field->new('952', '', '', 'a' => "Code" ) );
} else {
$record->append_fields( MARC::Field->new('008', ' lng' ) );
$record->append_fields( MARC::Field->new('245', '', '', 'a' => "Title $index" ) );
$record->append_fields( MARC::Field->new('952', '', '', 'a' => "Code" ) );
}
my ($biblionumber) = AddBiblio($record, '');
$first_bn = $biblionumber unless $first_bn;
my $timestamp = $base_datetime->ymd . ' ' .$base_datetime->hms;
$sth->execute($timestamp,$biblionumber);
$sth2->execute($timestamp,$biblionumber);
$timestamp .= 'Z';
$timestamp =~ s/ /T/;
$record = GetMarcBiblio({ biblionumber => $biblionumber });
my $record_transformed = $record->clone;
$record_transformed->delete_fields( $record_transformed->field('952'));
$record_transformed = XMLin($record_transformed->as_xml_record);
$record = XMLin($record->as_xml_record);
push @header, { datestamp => $timestamp, identifier => "TEST:$biblionumber" };
my $dc = {
'dc:title' => "Title $index",
'dc:language' => "lng",
'dc:type' => {},
'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
'xmlns:oai_dc' => 'http://www.openarchives.org/OAI/2.0/oai_dc/',
'xmlns:dc' => 'http://purl.org/dc/elements/1.1/',
'xsi:schemaLocation' => 'http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd',
};
if (C4::Context->preference('marcflavour') eq 'UNIMARC') {
$dc->{'dc:identifier'} = $biblionumber;
}
push @oaidc, {
header => $header[$index],
metadata => {
'oai_dc:dc' => $dc,
},
};
push @marcxml, {
header => $header[$index],
metadata => {
record => $record,
},
};
push @marcxml_transformed, {
header => $header[$index],
metadata => {
record => $record_transformed,
},
};
}
my $syspref = {
'LibraryName' => 'My Library',
'OAI::PMH' => 1,
'OAI-PMH:archiveID' => 'TEST',
'OAI-PMH:ConfFile' => '',
'OAI-PMH:MaxCount' => 3,
'OAI-PMH:DeletedRecord' => 'persistent',
};
while ( my ($name, $value) = each %$syspref ) {
t::lib::Mocks::mock_preference( $name => $value );
}
sub test_query {
my ($test, $param, $expected) = @_;
%param = %$param;
my %full_expected = (
%$expected,
(
request => 'http://localhost',
xmlns => 'http://www.openarchives.org/OAI/2.0/',
'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
'xsi:schemaLocation' => 'http://www.openarchives.org/OAI/2.0/ http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd',
)
);
my $response;
{
my $stdout;
local *STDOUT;
open STDOUT, '>', \$stdout;
Koha::OAI::Server::Repository->new();
$response = XMLin($stdout);
}
delete $response->{responseDate};
unless (cmp_deeply($response, \%full_expected, $test)) {
diag
"PARAM:" . YAML::XS::Dump($param) .
"EXPECTED:" . YAML::XS::Dump(\%full_expected) .
"RESPONSE:" . YAML::XS::Dump($response);
}
}
test_query('ListMetadataFormats', {verb => 'ListMetadataFormats'}, {
ListMetadataFormats => {
metadataFormat => [
{
metadataNamespace => 'http://www.openarchives.org/OAI/2.0/oai_dc/',
metadataPrefix=> 'oai_dc',
schema => 'http://www.openarchives.org/OAI/2.0/oai_dc.xsd',
},
{
metadataNamespace => 'http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim',
metadataPrefix => 'marc21',
schema => 'http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd',
},
{
metadataNamespace => 'http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim',
metadataPrefix => 'marcxml',
schema => 'http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd',
},
],
},
});
test_query('ListIdentifiers without metadataPrefix', {verb => 'ListIdentifiers'}, {
error => {
code => 'badArgument',
content => "Required argument 'metadataPrefix' was undefined",
},
});
test_query('ListIdentifiers', {verb => 'ListIdentifiers', metadataPrefix => 'marcxml'}, {
ListIdentifiers => {
header => [ @header[0..2] ],
resumptionToken => {
content => re( qr{^marcxml/3////0/0/\d+$} ),
cursor => 3,
},
},
});
test_query('ListIdentifiers', {verb => 'ListIdentifiers', metadataPrefix => 'marcxml'}, {
ListIdentifiers => {
header => [ @header[0..2] ],
resumptionToken => {
content => re( qr{^marcxml/3////0/0/\d+$} ),
cursor => 3,
},
},
});
test_query(
'ListIdentifiers with resumptionToken 1',
{ verb => 'ListIdentifiers', resumptionToken => "marcxml/3/1970-01-01T00:00:00Z/$date_to//0/0/" . ($first_bn + 3) },
{
ListIdentifiers => {
header => [ @header[3..5] ],
resumptionToken => {
content => re( qr{^marcxml/6/1970-01-01T00:00:00Z/$date_to//0/0/\d+$} ),
cursor => 6,
},
},
},
);
test_query(
'ListIdentifiers with resumptionToken 2',
{ verb => 'ListIdentifiers', resumptionToken => "marcxml/6/1970-01-01T00:00:00Z/$date_to//0/0/" . ($first_bn + 6) },
{
ListIdentifiers => {
header => [ @header[6..8] ],
resumptionToken => {
content => re( qr{^marcxml/9/1970-01-01T00:00:00Z/$date_to//0/0/\d+$} ),
cursor => 9,
},
},
},
);
test_query(
'ListIdentifiers with resumptionToken 3, response without resumption',
{ verb => 'ListIdentifiers', resumptionToken => "marcxml/9/1970-01-01T00:00:00Z/$date_to//0/0/" . ($first_bn + 9) },
{
ListIdentifiers => {
header => $header[9],
},
},
);
test_query('ListRecords marcxml without metadataPrefix', {verb => 'ListRecords'}, {
error => {
code => 'badArgument',
content => "Required argument 'metadataPrefix' was undefined",
},
});
test_query('ListRecords marcxml', {verb => 'ListRecords', metadataPrefix => 'marcxml'}, {
ListRecords => {
record => [ @marcxml[0..2] ],
resumptionToken => {
content => re( qr{^marcxml/3////0/0/\d+$} ),
cursor => 3,
},
},
});
test_query(
'ListRecords marcxml with resumptionToken 1',
{ verb => 'ListRecords', resumptionToken => "marcxml/3////0/0/" . ($first_bn + 3) },
{ ListRecords => {
record => [ @marcxml[3..5] ],
resumptionToken => {
content => re( qr{^marcxml/6////0/0/\d+$} ),
cursor => 6,
},
},
});
test_query(
'ListRecords marcxml with resumptionToken 2',
{ verb => 'ListRecords', resumptionToken => "marcxml/6/1970-01-01T00:00:00Z/$date_to//0/0/" . ($first_bn + 6) },
{ ListRecords => {
record => [ @marcxml[6..8] ],
resumptionToken => {
content => re( qr{^marcxml/9/1970-01-01T00:00:00Z/$date_to//0/0/\d+$} ),
cursor => 9,
},
},
});
# Last record, so no resumption token
test_query(
'ListRecords marcxml with resumptionToken 3, response without resumption',
{ verb => 'ListRecords', resumptionToken => "marcxml/9/1970-01-01T00:00:00Z/$date_to//0/0/" . ($first_bn + 9) },
{ ListRecords => {
record => $marcxml[9],
},
});
test_query('ListRecords oai_dc', {verb => 'ListRecords', metadataPrefix => 'oai_dc'}, {
ListRecords => {
record => [ @oaidc[0..2] ],
resumptionToken => {
content => re( qr{^oai_dc/3////0/0/\d+$} ),
cursor => 3,
},
},
});
test_query(
'ListRecords oai_dc with resumptionToken 1',
{ verb => 'ListRecords', resumptionToken => "oai_dc/3////0/0/" . ($first_bn + 3) },
{ ListRecords => {
record => [ @oaidc[3..5] ],
resumptionToken => {
content => re( qr{^oai_dc/6////0/0/\d+$} ),
cursor => 6,
},
},
});
test_query(
'ListRecords oai_dc with resumptionToken 2',
{ verb => 'ListRecords', resumptionToken => "oai_dc/6/1970-01-01T00:00:00Z/$date_to//0/0/" . ($first_bn + 6) },
{ ListRecords => {
record => [ @oaidc[6..8] ],
resumptionToken => {
content => re( qr{^oai_dc/9/1970-01-01T00:00:00Z/$date_to//0/0/\d+$} ),
cursor => 9,
},
},
});
# Last record, so no resumption token
test_query(
'ListRecords oai_dc with resumptionToken 3, response without resumption',
{ verb => 'ListRecords', resumptionToken => "oai_dc/9/1970-01-01T00:00:00Z/$date_to//0/0/" . ($first_bn + 9) },
{ ListRecords => {
record => $oaidc[9],
},
});
# List records, but now transformed by XSLT
t::lib::Mocks::mock_preference("OAI-PMH:ConfFile" => File::Spec->rel2abs(dirname(__FILE__)) . "/oaiconf.yaml");
test_query('ListRecords marcxml with xsl transformation',
{ verb => 'ListRecords', metadataPrefix => 'marcxml' },
{ ListRecords => {
record => [ @marcxml_transformed[0..2] ],
resumptionToken => {
content => re( qr{^marcxml/3////0/0/\d+$} ),
cursor => 3,
}
},
});
t::lib::Mocks::mock_preference("OAI-PMH:ConfFile" => '');
restore_time();
subtest 'Bug 19725: OAI-PMH ListRecords and ListIdentifiers should use biblio_metadata.timestamp' => sub {
plan tests => 1;
# Wait 1 second to be sure no timestamp will be equal to $from defined below
sleep 1;
# Modify record to trigger auto update of timestamp
(my $biblionumber = $marcxml[0]->{header}->{identifier}) =~ s/^.*:(.*)/$1/;
my $record = GetMarcBiblio({biblionumber => $biblionumber});
$record->append_fields(MARC::Field->new(999, '', '', z => '_'));
ModBiblio( $record, $biblionumber );
my $from_dt = dt_from_string(
Koha::Biblio::Metadatas->find({ biblionumber => $biblionumber, format => 'marcxml', schema => 'MARC21' })->timestamp
);
my $from = $from_dt->ymd . 'T' . $from_dt->hms . 'Z';
$oaidc[0]->{header}->{datestamp} = $from;
test_query(
'ListRecords oai_dc with parameter from',
{ verb => 'ListRecords', metadataPrefix => 'oai_dc', from => $from },
{ ListRecords => {
record => $oaidc[0],
},
});
};
subtest 'Bug 20665: OAI-PMH Provider should reset the MySQL connection time zone' => sub {
plan tests => 2;
# Set time zone to SYSTEM so that it can be checked later
$dbh->do("SET time_zone='SYSTEM'");
test_query('ListIdentifiers without metadataPrefix', {verb => 'ListIdentifiers'}, {
error => {
code => 'badArgument',
content => "Required argument 'metadataPrefix' was undefined",
},
});
my $sth = C4::Context->dbh->prepare('SELECT @@session.time_zone');
$sth->execute();
my ( $tz ) = $sth->fetchrow();
ok ( $tz eq 'SYSTEM', 'MySQL connection time zone is SYSTEM' );
};
$schema->storage->txn_rollback;
subtest 'ListSets tests' => sub {
plan tests => 3;
t::lib::Mocks::mock_preference( 'OAI::PMH' => 1 );
t::lib::Mocks::mock_preference( 'OAI-PMH:MaxCount' => 3 );
$schema->storage->txn_begin;
$dbh->do('DELETE FROM oai_sets');
# Add a bunch of sets
my @first_page_sets = ();
for my $i ( 1 .. 3 ) {
AddOAISet(
{ 'spec' => "setSpec_$i",
'name' => "setName_$i",
}
);
push @first_page_sets, { setSpec => "setSpec_$i", setName => "setName_$i" };
}
# Add more to force pagination
my @second_page_sets = ();
for my $i ( 4 .. 6 ) {
AddOAISet(
{ 'spec' => "setSpec_$i",
'name' => "setName_$i",
}
);
push @second_page_sets, { setSpec => "setSpec_$i", setName => "setName_$i" };
}
AddOAISet(
{ 'spec' => "setSpec_7",
'name' => "setName_7",
}
);
test_query(
'ListSets',
{ verb => 'ListSets' },
{ ListSets => {
resumptionToken => {
content => re( qr{^/3////1/0/4$} ),
cursor => 3,
},
set => \@first_page_sets
}
}
);
test_query(
'ListSets',
{ verb => 'ListSets', resumptionToken => '/3////1/0/4' },
{ ListSets => {
resumptionToken => {
content => re( qr{^/6////1/0/7$} ),
cursor => 6,
},
set => \@second_page_sets
}
}
);
test_query(
'ListSets',
{ verb => 'ListSets', resumptionToken => "/6////1/0/7" },
{ ListSets => {
set => { setSpec => "setSpec_7", setName => "setName_7" }
}
}
);
$schema->storage->txn_rollback;
};
subtest 'Tests for timestamp handling' => sub {
plan tests => 27;
t::lib::Mocks::mock_preference( 'OAI::PMH' => 1 );
t::lib::Mocks::mock_preference( 'OAI-PMH:MaxCount' => 3 );
t::lib::Mocks::mock_preference( 'OAI-PMH:ConfFile' => File::Spec->rel2abs(dirname(__FILE__)) . '/oaiconf_items.yaml' );
$schema->storage->txn_begin;
my $sth_metadata = $dbh->prepare('UPDATE biblio_metadata SET timestamp=? WHERE biblionumber=?');
my $sth_del_metadata = $dbh->prepare('UPDATE deletedbiblio_metadata SET timestamp=? WHERE biblionumber=?');
my $sth_item = $dbh->prepare('UPDATE items SET timestamp=? WHERE itemnumber=?');
my $sth_del_item = $dbh->prepare('UPDATE deleteditems SET timestamp=? WHERE itemnumber=?');
my $builder = t::lib::TestBuilder->new;
set_fixed_time(CORE::time());
my $utc_datetime = dt_from_string(undef, undef, 'UTC');
my $utc_timestamp = $utc_datetime->ymd . 'T' . $utc_datetime->hms . 'Z';
my $timestamp = dt_from_string(undef, 'sql');
# Test a bib with one item
my $biblio1 = $builder->build_sample_biblio();
$sth_metadata->execute($timestamp, $biblio1->biblionumber);
my $item1 = $builder->build_sample_item(
{
biblionumber => $biblio1->biblionumber
}
);
$sth_item->execute($timestamp, $item1->itemnumber);
my $list_items = {
verb => 'ListRecords',
metadataPrefix => 'marc21',
from => $utc_timestamp
};
my $list_no_items = {
verb => 'ListRecords',
metadataPrefix => 'marcxml',
from => $utc_timestamp
};
my $get_items = {
verb => 'GetRecord',
metadataPrefix => 'marc21',
identifier => 'TEST:' . $biblio1->biblionumber
};
my $get_no_items = {
verb => 'GetRecord',
metadataPrefix => 'marcxml',
identifier => 'TEST:' . $biblio1->biblionumber
};
my $expected = {
record => {
header => {
datestamp => $utc_timestamp,
identifier => 'TEST:' . $biblio1->biblionumber
},
metadata => {
record => XMLin(
GetMarcBiblio({ biblionumber => $biblio1->biblionumber, embed_items => 1, opac => 1 })->as_xml_record()
)
}
}
};
my $expected_no_items = {
record => {
header => {
datestamp => $utc_timestamp,
identifier => 'TEST:' . $biblio1->biblionumber
},
metadata => {
record => XMLin(
GetMarcBiblio({ biblionumber => $biblio1->biblionumber, embed_items => 0, opac => 1 })->as_xml_record()
)
}
}
};
test_query(
'ListRecords - biblio with a single item',
$list_items,
{ ListRecords => $expected }
);
test_query(
'ListRecords - biblio with a single item (items not returned)',
$list_no_items,
{ ListRecords => $expected_no_items }
);
test_query(
'GetRecord - biblio with a single item',
$get_items,
{ GetRecord => $expected }
);
test_query(
'GetRecord - biblio with a single item (items not returned)',
$get_no_items,
{ GetRecord => $expected_no_items }
);
# Add an item 10 seconds later and check results
set_relative_time(10);
$utc_datetime = dt_from_string(undef, undef, 'UTC');
$utc_timestamp = $utc_datetime->ymd . 'T' . $utc_datetime->hms . 'Z';
$timestamp = dt_from_string(undef, 'sql');
my $item2 = $builder->build_sample_item(
{
biblionumber => $biblio1->biblionumber
}
);
$sth_item->execute($timestamp, $item2->itemnumber);
$expected->{record}{header}{datestamp} = $utc_timestamp;
$expected->{record}{metadata}{record} = XMLin(
GetMarcBiblio({ biblionumber => $biblio1->biblionumber, embed_items => 1, opac => 1 })->as_xml_record()
);
test_query(
'ListRecords - biblio with two items',
$list_items,
{ ListRecords => $expected }
);
test_query(
'ListRecords - biblio with two items (items not returned)',
$list_no_items,
{ ListRecords => $expected_no_items }
);
test_query(
'GetRecord - biblio with a two items',
$get_items,
{ GetRecord => $expected }
);
test_query(
'GetRecord - biblio with a two items (items not returned)',
$get_no_items,
{ GetRecord => $expected_no_items }
);
# Set biblio timestamp 10 seconds later and check results
set_relative_time(10);
$utc_datetime = dt_from_string(undef, undef, 'UTC');
$utc_timestamp= $utc_datetime->ymd . 'T' . $utc_datetime->hms . 'Z';
$timestamp = dt_from_string(undef, 'sql');
$sth_metadata->execute($timestamp, $biblio1->biblionumber);
$expected->{record}{header}{datestamp} = $utc_timestamp;
$expected_no_items->{record}{header}{datestamp} = $utc_timestamp;
test_query(
"ListRecords - biblio with timestamp higher than item's",
$list_items,
{ ListRecords => $expected }
);
test_query(
"ListRecords - biblio with timestamp higher than item's (items not returned)",
$list_no_items,
{ ListRecords => $expected_no_items }
);
test_query(
"GetRecord - biblio with timestamp higher than item's",
$get_items,
{ GetRecord => $expected }
);
test_query(
"GetRecord - biblio with timestamp higher than item's (items not returned)",
$get_no_items,
{ GetRecord => $expected_no_items }
);
# Delete an item 10 seconds later and check results
set_relative_time(10);
$utc_datetime = dt_from_string(undef, undef, 'UTC');
$utc_timestamp = $utc_datetime->ymd . 'T' . $utc_datetime->hms . 'Z';
$item1->safe_delete({ skip_record_index =>1 });
$sth_del_item->execute($timestamp, $item1->itemnumber);
$expected->{record}{header}{datestamp} = $utc_timestamp;
$expected->{record}{metadata}{record} = XMLin(
GetMarcBiblio({ biblionumber => $biblio1->biblionumber, embed_items => 1, opac => 1 })->as_xml_record()
);
test_query(
'ListRecords - biblio with existing and deleted item',
$list_items,
{ ListRecords => $expected }
);
test_query(
'ListRecords - biblio with existing and deleted item (items not returned)',
$list_no_items,
{ ListRecords => $expected_no_items }
);
test_query(
'GetRecord - biblio with existing and deleted item',
$get_items,
{ GetRecord => $expected }
);
test_query(
'GetRecord - biblio with existing and deleted item (items not returned)',
$get_no_items,
{ GetRecord => $expected_no_items }
);
# Delete also the second item and verify results
$item2->safe_delete({ skip_record_index =>1 });
$sth_del_item->execute($timestamp, $item2->itemnumber);
$expected->{record}{metadata}{record} = XMLin(
GetMarcBiblio({ biblionumber => $biblio1->biblionumber, embed_items => 1, opac => 1 })->as_xml_record()
);
test_query(
'ListRecords - biblio with two deleted items',
$list_items,
{ ListRecords => $expected }
);
test_query(
'ListRecords - biblio with two deleted items (items not returned)',
$list_no_items,
{ ListRecords => $expected_no_items }
);
test_query(
'GetRecord - biblio with two deleted items',
$get_items,
{ GetRecord => $expected }
);
test_query(
'GetRecord - biblio with two deleted items (items not returned)',
$get_no_items,
{ GetRecord => $expected_no_items }
);
# Delete the biblio 10 seconds later and check results
set_relative_time(10);
$utc_datetime = dt_from_string(undef, undef, 'UTC');
$utc_timestamp = $utc_datetime->ymd . 'T' . $utc_datetime->hms . 'Z';
$timestamp = dt_from_string(undef, 'sql');
is(undef, DelBiblio($biblio1->biblionumber, { skip_record_index =>1 }), 'Biblio deleted');
$sth_del_metadata->execute($timestamp, $biblio1->biblionumber);
my $expected_header = {
record => {
header => {
datestamp => $utc_timestamp,
identifier => 'TEST:' . $biblio1->biblionumber,
status => 'deleted'
}
}
};
test_query(
'ListRecords - deleted biblio with two deleted items',
$list_items,
{ ListRecords => $expected_header }
);
test_query(
'ListRecords - deleted biblio with two deleted items (items not returned)',
$list_no_items,
{ ListRecords => $expected_header }
);
test_query(
'GetRecord - deleted biblio with two deleted items',
$get_items,
{ GetRecord => $expected_header }
);
test_query(
'GetRecord - deleted biblio with two deleted items (items not returned)',
$get_no_items,
{ GetRecord => $expected_header }
);
# Add a second biblio 10 seconds later and check that both are returned properly
set_relative_time(10);
$utc_datetime = dt_from_string(undef, undef, 'UTC');
$utc_timestamp = $utc_datetime->ymd . 'T' . $utc_datetime->hms . 'Z';
$timestamp = dt_from_string(undef, 'sql');
my $biblio2 = $builder->build_sample_biblio();
$sth_metadata->execute($timestamp, $biblio2->biblionumber);
my $expected2 = {
record => [
$expected_header->{record},
{
header => {
datestamp => $utc_timestamp,
identifier => 'TEST:' . $biblio2->biblionumber
},
metadata => {
record => XMLin(
GetMarcBiblio({ biblionumber => $biblio2->biblionumber, embed_items => 1, opac => 1 })->as_xml_record()
)
}
}
]
};
my $expected2_no_items = {
record => [
$expected_header->{record},
{
header => {
datestamp => $utc_timestamp,
identifier => 'TEST:' . $biblio2->biblionumber
},
metadata => {
record => XMLin(
GetMarcBiblio({ biblionumber => $biblio2->biblionumber, embed_items => 0, opac => 1 })->as_xml_record()
)
}
}
]
};
test_query(
'ListRecords - deleted biblio and normal biblio',
$list_items,
{ ListRecords => $expected2 }
);
test_query(
'ListRecords - deleted biblio and normal biblio (items not returned)',
$list_no_items,
{ ListRecords => $expected2_no_items }
);
restore_time();
$schema->storage->txn_rollback;
};