#!/usr/bin/perl # 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 . use Modern::Perl; use Try::Tiny; use POSIX qw(floor); use MARC::Record; use C4::Context; use C4::Biblio qw( AddBiblio ModBiblio DelBiblio ); use Koha::Database; use Koha::Biblios; use Test::More tests => 24; use Test::MockModule; use Koha::MarcOverlayRules; use t::lib::Mocks; my $schema = Koha::Database->schema; $schema->storage->txn_begin; t::lib::Mocks::mock_preference('MARCOverlayRules', '1'); Koha::MarcOverlayRules->search->delete; sub build_record { my ( $fields ) = @_; my $record = MARC::Record->new; my @marc_fields; for my $f ( @$fields ) { my $tag = $f->[0]; my @subfields; for my $i ( 1 .. (scalar(@$f) / 2) ) { my $ii = floor($i*2); push @subfields, $f->[$ii - 1], $f->[$ii]; } push @marc_fields, MARC::Field->new($tag, '','', @subfields); } $record->append_fields(@marc_fields); return $record; } # Create a record my $orig_record = build_record([ [ '250', 'a', '250 bottles of beer on the wall' ], [ '250', 'a', '256 bottles of beer on the wall' ], [ '500', 'a', 'One bottle of beer in the fridge' ], ] ); my $incoming_record = build_record( [ ['250', 'a', '256 bottles of beer on the wall'], # Unchanged ['250', 'a', '251 bottles of beer on the wall'], # Appended #['250', 'a', '250 bottles of beer on the wall'], # Removed #['500', 'a', 'One bottle of beer in the fridge'], # Deleted ['501', 'a', 'One cold bottle of beer in the fridge'], # Added ['501', 'a', 'Two cold bottles of beer in the fridge'], # Added ] ); # Test default behavior when MARCOverlayRules is enabled, but no rules defined (overwrite) subtest 'No rule defined' => sub { plan tests => 1; my $merged_record = Koha::MarcOverlayRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' }); is( $merged_record->as_formatted, $incoming_record->as_formatted, 'Incoming record used as it with no rules defined' ); }; my $rule = Koha::MarcOverlayRules->find_or_create({ tag => '*', module => 'source', filter => '*', add => 0, append => 0, remove => 0, delete => 0 }); subtest 'Record fields has been protected when matched merge all rule operations are set to "0"' => sub { plan tests => 1; my $merged_record = Koha::MarcOverlayRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' }); is( $merged_record->as_formatted, $orig_record->as_formatted, 'Record not modified if all op=0' ); }; subtest '"Add new" - Only new fields has been added when add = 1, append = 0, remove = 0, delete = 0' => sub { plan tests => 1; $rule->set( { 'add' => 1, 'append' => 0, 'remove' => 0, 'delete' => 0, } )->store(); my $merged_record = Koha::MarcOverlayRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' }); my $expected_record = build_record([ [ '250', 'a', '250 bottles of beer on the wall' ], # original [ '250', 'a', '256 bottles of beer on the wall' ], # original [ '500', 'a', 'One bottle of beer in the fridge' ], # original [ '501', 'a', 'One cold bottle of beer in the fridge' ], # incoming [ '501', 'a', 'Two cold bottles of beer in the fridge' ], # incoming ]); is( $merged_record->as_formatted, $expected_record->as_formatted, 'Add fields from the incoming record that are not in the original record' ); }; subtest 'Only appended fields has been added when add = 0, append = 1, remove = 0, delete = 0' => sub { plan tests => 1; $rule->set( { 'add' => 0, 'append' => 1, 'remove' => 0, 'delete' => 0, } )->store; my $merged_record = Koha::MarcOverlayRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' }); my $expected_record = build_record([ [ '250', 'a', '250 bottles of beer on the wall' ], # original [ '250', 'a', '256 bottles of beer on the wall' ], # original # "251" field has been appended [ '250', 'a', '251 bottles of beer on the wall' ], # incoming # "500" field has retained its original value [ '500', 'a', 'One bottle of beer in the fridge' ], # original ]); is( $merged_record->as_formatted, $expected_record->as_formatted, 'Add fields from the incoming record that are in the original record' ); }; subtest '"Add and append" - add = 1, append = 1, remove = 0, delete = 0' => sub { plan tests => 1; $rule->set( { 'add' => 1, 'append' => 1, 'remove' => 0, 'delete' => 0, } )->store; my $expected_record = build_record([ [ '250', 'a', '250 bottles of beer on the wall' ], # original [ '250', 'a', '256 bottles of beer on the wall' ], # original # "251" field has been appended [ '250', 'a', '251 bottles of beer on the wall' ], # incoming # "500" field has retained its original value [ '500', 'a', 'One bottle of beer in the fridge' ], # original # "501" fields have been added [ '501', 'a', 'One cold bottle of beer in the fridge' ], # incoming [ '501', 'a', 'Two cold bottles of beer in the fridge' ], # incoming ]); my $merged_record = Koha::MarcOverlayRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' }); is( $merged_record->as_formatted, $expected_record->as_formatted, 'Appended and added fields have been added' ); }; subtest 'Record fields has been only removed when add = 0, append = 0, remove = 1, delete = 0' => sub { plan tests => 1; $rule->set( { 'add' => 0, 'append' => 0, 'remove' => 1, 'delete' => 0, } )->store; # Warning - not obvious as the 500 is untouched my $expected_record = build_record([ # "250" field has been removed [ '250', 'a', '256 bottles of beer on the wall' ], # original and incoming # "500" field has retained its original value [ '500', 'a', 'One bottle of beer in the fridge' ], # original ]); my $merged_record = Koha::MarcOverlayRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' }); is( $merged_record->as_formatted, $expected_record->as_formatted, 'Fields not in the incoming record are removed' ); }; subtest 'Record fields has been added and removed when add = 1, append = 0, remove = 1, delete = 0' => sub { plan tests => 1; $rule->set( { 'add' => 1, 'append' => 0, 'remove' => 1, 'delete' => 0, } )->store(); my $expected_record = build_record([ # "250" field has been removed [ '250', 'a', '256 bottles of beer on the wall' ], # original and incoming # "500" field has retained its original value [ '500', 'a', 'One bottle of beer in the fridge' ], # original # "501" fields have been added [ '501', 'a', 'One cold bottle of beer in the fridge' ], # incoming [ '501', 'a', 'Two cold bottles of beer in the fridge' ], # incoming ]); my $merged_record = Koha::MarcOverlayRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' }); is( $merged_record->as_formatted, $expected_record->as_formatted, 'Fields not in the incoming record are removed, fields not in the original record have been added' ); }; subtest 'Record fields has been appended and removed when add = 0, append = 1, remove = 1, delete = 0' => sub { plan tests => 1; $rule->set( { 'add' => 0, 'append' => 1, 'remove' => 1, 'delete' => 0, } )->store(); my $expected_record = build_record([ # "250" field has been appended and removed [ '250', 'a', '256 bottles of beer on the wall' ], # incoming [ '250', 'a', '251 bottles of beer on the wall' ], # incoming # "500" field has retained its original value [ '500', 'a', 'One bottle of beer in the fridge' ], # original ]); my $merged_record = Koha::MarcOverlayRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' }); is( $merged_record->as_formatted, $expected_record->as_formatted, 'Fields in the incoming record replace fields from the original record, fields only in the original record has been kept, fields not in the original record have been skipped' ); }; subtest 'Record fields has been added, appended and removed when add = 0, append = 1, remove = 1, delete = 0' => sub { plan tests => 1; $rule->set( { 'add' => 1, 'append' => 1, 'remove' => 1, 'delete' => 0, } )->store(); my $expected_record = build_record([ # "250" field has been appended and removed [ '250', 'a', '256 bottles of beer on the wall' ], # incoming [ '250', 'a', '251 bottles of beer on the wall' ], # incoming # "500" field has retained its original value [ '500', 'a', 'One bottle of beer in the fridge' ], # original # "501" fields have been added [ '501', 'a', 'One cold bottle of beer in the fridge' ], # incoming [ '501', 'a', 'Two cold bottles of beer in the fridge' ], # incoming ]); my $merged_record = Koha::MarcOverlayRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' }); is( $merged_record->as_formatted, $expected_record->as_formatted, 'Fields in the incoming record replace fields from the original record, fields only in the original record has been kept, fields not in the original record have been added' ); }; subtest 'Record fields has been deleted when add = 0, append = 0, remove = 0, delete = 1' => sub { plan tests => 1; $rule->set( { 'add' => 0, 'append' => 0, 'remove' => 0, 'delete' => 1, } )->store(); # FIXME the tooltip for delete is saying # "If the original record has fields matching the rule tag, but no fields with this are found in the incoming record" # But it does not seem to do that my $expected_record = build_record([ # "250" fields have retained their original value [ '250', 'a', '250 bottles of beer on the wall' ], # original [ '250', 'a', '256 bottles of beer on the wall' ], # original ]); my $merged_record = Koha::MarcOverlayRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' }); is( $merged_record->as_formatted, $expected_record->as_formatted, 'Only fields in original and incoming are kept, but incoming values are ignored' ); }; subtest 'Record fields has been added and deleted when add = 1, append = 0, remove = 0, delete = 1' => sub { plan tests => 1; $rule->set( { 'add' => 1, 'append' => 0, 'remove' => 0, 'delete' => 1, } )->store(); # Warning - is there a use case in real-life for this combinaison? my $expected_record = build_record([ # "250" field have retained their original value [ '250', 'a', '250 bottles of beer on the wall' ], # original [ '250', 'a', '256 bottles of beer on the wall' ], # original # "500" field has been removed # "501" fields have been added [ '501', 'a', 'One cold bottle of beer in the fridge' ], # incoming [ '501', 'a', 'Two cold bottles of beer in the fridge' ], # incoming ]); my $merged_record = Koha::MarcOverlayRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' }); is( $merged_record->as_formatted, $expected_record->as_formatted, 'Fields from the incoming records are kept, but keep the value from the original record if they already existed' ); }; subtest 'Record fields has been appended and deleted when add = 0, append = 1, remove = 0, delete = 1' => sub { plan tests => 1; $rule->set( { 'add' => 0, 'append' => 1, 'remove' => 0, 'delete' => 1, } )->store(); my $expected_record = build_record([ # "250" field has been appended [ '250', 'a', '250 bottles of beer on the wall' ], # original [ '250', 'a', '256 bottles of beer on the wall' ], # original and incoming [ '250', 'a', '251 bottles of beer on the wall' ], # incoming # "500" field has been removed ]); my $merged_record = Koha::MarcOverlayRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' }); is( $merged_record->as_formatted, $expected_record->as_formatted, 'Only fields that already existed are appended' ); }; subtest 'Record fields has been added, appended and deleted when add = 1, append = 1, remove = 0, delete = 1' => sub { plan tests => 1; $rule->set( { 'add' => 1, 'append' => 1, 'remove' => 0, 'delete' => 1, } )->store(); my $expected_record = build_record([ # "250" field has been appended [ '250', 'a', '250 bottles of beer on the wall' ], # original [ '250', 'a', '256 bottles of beer on the wall' ], # original and incoming [ '250', 'a', '251 bottles of beer on the wall' ], # incoming # "500" field has been removed # "501" fields have been added [ '501', 'a', 'One cold bottle of beer in the fridge' ], # incoming [ '501', 'a', 'Two cold bottles of beer in the fridge' ], # incoming ]); my $merged_record = Koha::MarcOverlayRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' }); is( $merged_record->as_formatted, $expected_record->as_formatted, 'Fields in the incoming record are added and appended, fields not in the original record are removed' ); }; subtest 'Record fields has been removed and deleted when add = 0, append = 0, remove = 1, delete = 1' => sub { plan tests => 1; $rule->set( { 'add' => 0, 'append' => 0, 'remove' => 1, 'delete' => 1, } )->store(); my $expected_record = build_record([ # "250" field has been removed [ '250', 'a', '256 bottles of beer on the wall' ], # original and incoming # "500" field has been removed ]); my $merged_record = Koha::MarcOverlayRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' }); is( $merged_record->as_formatted, $expected_record->as_formatted, 'Union' ); }; subtest 'Record fields has been added, removed and deleted when add = 1, append = 0, remove = 1, delete = 1' => sub { plan tests => 1; $rule->set( { 'add' => 1, 'append' => 0, 'remove' => 1, 'delete' => 1, } )->store(); my $expected_record = build_record([ # "250" field has been appended [ '250', 'a', '256 bottles of beer on the wall' ], # original and incoming # "500" field has been removed # "501" fields have been added [ '501', 'a', 'One cold bottle of beer in the fridge' ], # incoming [ '501', 'a', 'Two cold bottles of beer in the fridge' ], # incoming ]); my $merged_record = Koha::MarcOverlayRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' }); is( $merged_record->as_formatted, $expected_record->as_formatted, 'Union for existing fields, new fields are added' ); }; subtest 'Record fields has been appended, removed and deleted when add = 0, append = 1, remove = 1, delete = 1' => sub { plan tests => 1; $rule->set( { 'add' => 0, 'append' => 1, 'remove' => 1, 'delete' => 1, } )->store(); my $expected_record = build_record([ # "250" field has been appended and removed [ '250', 'a', '256 bottles of beer on the wall' ], # incoming [ '250', 'a', '251 bottles of beer on the wall' ], # incoming # "500" field has been removed ]); my $merged_record = Koha::MarcOverlayRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' }); is( $merged_record->as_formatted, $expected_record->as_formatted, 'Fields from incoming replace original record. Existing fields not in incoming are removed' ); }; subtest 'Record fields has been overwritten when add = 1, append = 1, remove = 1, delete = 1' => sub { plan tests => 1; $rule->set( { 'add' => 1, 'append' => 1, 'remove' => 1, 'delete' => 1, } )->store(); my $merged_record = Koha::MarcOverlayRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' }); is( $merged_record->as_formatted, $incoming_record->as_formatted, 'Incoming record erase original record' ); }; subtest 'subfields order' => sub { plan tests => 2; $rule->set( { 'add' => 0, 'append' => 0, 'remove' => 0, 'delete' => 0, } )->store(); my $incoming_record = build_record( [ [ '250', 'a', '256 bottles of beer on the wall' ], [ '250', 'a', '250 bottles of beer on the wall' ], [ '500', 'a', 'One bottle of beer in the fridge' ], ] ); my $merged_record = Koha::MarcOverlayRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' }); is( $merged_record->as_formatted, $orig_record->as_formatted, 'Original record not modified - order of subfields not modified' ); $rule->set( { 'add' => 1, 'append' => 1, 'remove' => 1, 'delete' => 1, } )->store(); $merged_record = Koha::MarcOverlayRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' }); is( $merged_record->as_formatted, $incoming_record->as_formatted, 'Original record modified - order of subfields has been modified' ); }; # Test rule tag specificity # Protect field 500 with more specific tag value my $skip_all_rule = Koha::MarcOverlayRules->find_or_create({ tag => '500', module => 'source', filter => '*', add => 0, append => 0, remove => 0, delete => 0 }); subtest '"500" field has been protected when rule matching on tag "500" is add = 0, append = 0, remove = 0, delete = 0' => sub { plan tests => 1; my $expected_record = build_record([ # "250" field has been appended [ '250', 'a', '256 bottles of beer on the wall' ], # incoming [ '250', 'a', '251 bottles of beer on the wall' ], # incoming # "500" field has retained its original value [ '500', 'a', 'One bottle of beer in the fridge' ], # original # "501" fields have been added [ '501', 'a', 'One cold bottle of beer in the fridge' ], # incoming [ '501', 'a', 'Two cold bottles of beer in the fridge' ], # incoming ]); my $merged_record = Koha::MarcOverlayRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' }); is( $merged_record->as_formatted, $expected_record->as_formatted, 'All fields are erased by incoming record but 500 is protected' ); }; # Test regexp matching subtest '"5XX" fields has been protected when rule matching on regexp "5\d{2}" is add = 0, append = 0, remove = 0, delete = 0' => sub { plan tests => 1; $skip_all_rule->set( { 'tag' => '5\d{2}', } )->store; my $expected_record = build_record([ # "250" field has been appended [ '250', 'a', '256 bottles of beer on the wall' ], # incoming [ '250', 'a', '251 bottles of beer on the wall' ], # incoming # "500" field has retained its original value [ '500', 'a', 'One bottle of beer in the fridge' ], # original ]); my $merged_record = Koha::MarcOverlayRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' }); is( $merged_record->as_formatted, $expected_record->as_formatted, 'Fields are erased by incoming record but 500 is protected and 501 is not added' ); }; $skip_all_rule->delete(); # Test module specificity, the 0 all rule should no longer be included in set of applied rules subtest 'Record fields has been overwritten when non wild card rule with filter match is add = 1, append = 1, remove = 1, delete = 1' => sub { plan tests => 1; $rule->set( { 'filter' => 'test', } )->store(); my $merged_record = Koha::MarcOverlayRules->merge_records($orig_record, $incoming_record, { 'source' => 'test' }); is( $merged_record->as_formatted, $incoming_record->as_formatted, 'Only existing rule is not for us, erasing' # FIXME Is this comment correct? ); }; subtest 'An exception is thrown when append = 1, remove = 0 is set for control field rule' => sub { plan tests => 2; my $exception = try { Koha::MarcOverlayRules->validate({ 'tag' => '008', 'append' => 1, 'remove' => 0, }); } catch { return $_; }; ok(defined $exception, "Exception was caught"); ok($exception->isa('Koha::Exceptions::MarcOverlayRule::InvalidControlFieldActions'), "Exception is of correct class"); }; subtest 'An exception is thrown when rule tag is set to invalid regexp' => sub { plan tests => 2; my $exception = try { Koha::MarcOverlayRules->validate({ 'tag' => '**' }); } catch { return $_; }; ok(defined $exception, "Exception was caught"); ok($exception->isa('Koha::Exceptions::MarcOverlayRule::InvalidTagRegExp'), "Exception is of correct class"); }; subtest 'context option in ModBiblio is handled correctly' => sub { plan tests => 2; $rule->set( { tag => '250', module => 'source', filter => '*', 'add' => 0, 'append' => 0, 'remove' => 0, 'delete' => 0, } )->store(); my ($biblionumber, $biblioitemnumber) = AddBiblio($orig_record, ''); # Since marc merc rules are not run on save, only update # saved record should be identical to orig_record my $biblio = Koha::Biblios->find($biblionumber); my $saved_record = $biblio->metadata->record; my @all_fields = $saved_record->fields(); # Koha also adds 999c field, therefore 4 not 3 my $expected_record = build_record([ # "250" field has been appended [ '250', 'a', '250 bottles of beer on the wall' ], # original [ '250', 'a', '256 bottles of beer on the wall' ], # incoming [ '500', 'a', 'One bottle of beer in the fridge' ], # original [ '999', 'c', $biblionumber, 'd', $biblioitemnumber ], # created by AddBiblio ]); # Remove timestamp from saved record when comparing $saved_record->delete_fields( $saved_record->field('005') ); # Make sure leader is equal after AddBiblio $expected_record->leader($saved_record->leader()); is( $saved_record->as_formatted, $expected_record->as_formatted, ); $saved_record->append_fields( MARC::Field->new('250', '', '', 'a' => '251 bottles of beer on the wall'), # Appended MARC::Field->new('500', '', '', 'a' => 'One cold bottle of beer in the fridge'), # Appended ); ModBiblio($saved_record, $biblionumber, '', { overlay_context => { 'source' => 'test' } }); my $updated_record = $biblio->get_from_storage->metadata->record; $expected_record = build_record([ # "250" field has been appended [ '250', 'a', '250 bottles of beer on the wall' ], [ '250', 'a', '256 bottles of beer on the wall' ], [ '500', 'a', 'One bottle of beer in the fridge' ], [ '500', 'a', 'One cold bottle of beer in the fridge' ], [ '999', 'c', $biblionumber, 'd', $biblioitemnumber ], # created by AddBiblio ]); # Remove timestamp from saved record when comparing $updated_record->delete_fields( $updated_record->field('005') ); # Make sure leader is equal after ModBiblio $expected_record->leader($updated_record->leader()); is( $updated_record->as_formatted, $expected_record->as_formatted, ); # To trigger removal from search index etc DelBiblio($biblionumber); }; # Explicityly delete rule to trigger clearing of cache $rule->delete(); $schema->storage->txn_rollback;