From 86a4896fd1cbcbd96711d383d8340669798e826e Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Wed, 1 Jul 2026 09:21:40 +0100 Subject: [PATCH] Bug 30233: (follow-up) Add tests for _apply_substitution and combined modifiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - t/C4/ClassSplitRoutine/RegEx.t (new): exercises _apply_substitution via the public split_callnumber interface — basic substitution, alternate delimiters, \n/$n backrefs, /gi combined flag, /e rejection, and malformed or uncompilable rules (including the new logger warn path). - t/Koha/Regex/Replacement.t: extend the subst() helper to handle the ig/gi combined modifier and add a subtest that confirms case-insensitive global replacement and backref case preservation. Signed-off-by: Martin Renvoize Signed-off-by: Baptiste Wojtkowski --- t/C4/ClassSplitRoutine/RegEx.t | 76 ++++++++++++++++++++++++++++++++++ t/Koha/Regex/Replacement.t | 16 ++++++- 2 files changed, 90 insertions(+), 2 deletions(-) create mode 100755 t/C4/ClassSplitRoutine/RegEx.t diff --git a/t/C4/ClassSplitRoutine/RegEx.t b/t/C4/ClassSplitRoutine/RegEx.t new file mode 100755 index 0000000000..10fc63e584 --- /dev/null +++ b/t/C4/ClassSplitRoutine/RegEx.t @@ -0,0 +1,76 @@ +#!/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 Test::NoWarnings; +use Test::More tests => 6; + +use C4::ClassSplitRoutine::RegEx; + +# Exercise _apply_substitution through the public split_callnumber interface. +# split_callnumber($string, \@regexes) applies each regex in turn and splits +# on newlines; a single-rule list with no \n in the result returns a +# one-element list. +sub apply { + my ( $string, $regex ) = @_; + my @parts = C4::ClassSplitRoutine::RegEx::split_callnumber( $string, [$regex] ); + return $parts[0] // $string; +} + +subtest 'basic substitution' => sub { + plan tests => 3; + is( apply( 'foo', 's/foo/bar/' ), 'bar', 'simple replacement' ); + is( apply( 'foo', 's/foo/bar/g' ), 'bar', '/g flag' ); + is( apply( 'FOO', 's/foo/bar/i' ), 'bar', '/i flag' ); +}; + +subtest 'alternate delimiters' => sub { + plan tests => 3; + is( apply( 'foo', 's#foo#bar#' ), 'bar', 'hash delimiter' ); + is( apply( 'foo', 's|foo|bar|' ), 'bar', 'pipe delimiter' ); + is( apply( 'foo', 's!foo!bar!' ), 'bar', 'bang delimiter' ); +}; + +subtest 'backreference and flag combinations' => sub { + plan tests => 3; + is( apply( '20260623', 's/(\d{4})(\d{2})(\d{2})/$1-$2-$3/' ), '2026-06-23', '$n backrefs' ); + is( apply( '20260623', 's/(\d{4})(\d{2})(\d{2})/\1-\2-\3/' ), '2026-06-23', '\n backrefs' ); + is( apply( 'FOO foo', 's/foo/bar/gi' ), 'bar bar', '/gi combined' ); +}; + +subtest '/e flag is rejected (no code execution)' => sub { + plan tests => 3; + is( apply( 'foo', 's/foo/1+1/e' ), 'foo', '/e alone: string unchanged' ); + is( apply( 'foo', 's/foo/1+1/ge' ), 'foo', '/ge: string unchanged' ); + is( apply( 'foo', 's/foo/1+1/ige' ), 'foo', '/ige: string unchanged' ); +}; + +subtest 'malformed and uncompilable rules are silently ignored' => sub { + plan tests => 3; + + # Not an s/// at all — ignored + is( apply( 'foo', 'm/foo/' ), 'foo', 'non-substitution regex ignored' ); + + # Missing closing delimiter + is( apply( 'foo', 's/foo/bar' ), 'foo', 'truncated s/// ignored' ); + + # Invalid pattern — compilation fails (also exercises the logger warn path) + my @warnings; + local $SIG{__WARN__} = sub { push @warnings, @_ }; + is( apply( 'foo', 's/[invalid/bar/' ), 'foo', 'bad pattern ignored' ); +}; diff --git a/t/Koha/Regex/Replacement.t b/t/Koha/Regex/Replacement.t index c873f02e4f..9dcdc04a27 100755 --- a/t/Koha/Regex/Replacement.t +++ b/t/Koha/Regex/Replacement.t @@ -18,7 +18,7 @@ use Modern::Perl; use Test::NoWarnings; -use Test::More tests => 5; +use Test::More tests => 6; use File::Temp qw( tempdir ); use File::Spec; @@ -31,7 +31,8 @@ sub subst { my ( $value, $search, $replacement, $modifiers ) = @_; $modifiers //= ''; my $expand = sub { Koha::Regex::Replacement::expand_template( $replacement, [ @{^CAPTURE} ], {%+} ) }; - if ( $modifiers eq 'g' ) { $value =~ s/$search/$expand->()/ge } + if ( $modifiers =~ /g/ && $modifiers =~ /i/ ) { $value =~ s/$search/$expand->()/ige } + elsif ( $modifiers eq 'g' ) { $value =~ s/$search/$expand->()/ge } elsif ( $modifiers eq 'i' ) { $value =~ s/$search/$expand->()/ie } else { $value =~ s/$search/$expand->()/e } return $value; @@ -79,6 +80,17 @@ subtest 'legacy \1 backreferences and fixed escapes' => sub { is( subst( 'x', '(x)', '\$1' ), '$1', '\$ emits a literal dollar, not a backref' ); }; +subtest 'combined ig/gi modifiers' => sub { + plan tests => 3; + is( subst( 'FOO foo', 'foo', 'bar', 'ig' ), 'bar bar', '/ig: case-insensitive and global' ); + is( subst( 'FOO foo', 'foo', 'bar', 'gi' ), 'bar bar', '/gi: same with reversed flag order' ); + is( + subst( 'FOO foo', '(foo)', '[$1]', 'ig' ), + '[FOO] [foo]', + '/ig: backrefs preserve original case per match' + ); +}; + subtest 'malicious replacements are inert (no code execution)' => sub { plan tests => 4;