From e6142f90224c6d3e08d93bfac6b82e658ce7ef28 Mon Sep 17 00:00:00 2001 From: Marcel de Rooy Date: Thu, 27 Jul 2023 09:37:39 +0000 Subject: [PATCH] Bug 34430: Module for validating input Content-Type: text/plain; charset=utf-8 Test plan: Run t/Koha/Validator.t --- Koha/Validator.pm | 342 ++++++++++++++++++++++++++++++++++++++++ t/Koha/Validator.t | 379 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 721 insertions(+) create mode 100644 Koha/Validator.pm create mode 100755 t/Koha/Validator.t diff --git a/Koha/Validator.pm b/Koha/Validator.pm new file mode 100644 index 0000000000..dc072f9dcb --- /dev/null +++ b/Koha/Validator.pm @@ -0,0 +1,342 @@ +package Koha::Validator; + +# Copyright 2023 Rijksmuseum, Koha development team +# +# 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 . + +=head1 NAME + +Koha::Validator - Facilitate validation of sets of values + +=head1 SYNOPSIS + + use Koha::Validator; + #TODO + +=head1 DESCRIPTION + + This modules allows you to load rule sets using predefined rules, + regular expressions and validation routines, and pass sets of values + in order to apply those rules to them. + +=cut + +use Modern::Perl; +use Data::Dumper qw/Dumper/; #FIXME +use List::Util qw(any); +use Try::Tiny; + +use Koha::Exceptions; + +use constant OPTIONS => qw/ allow_missing_rules break_on_error replace_inline restrict_postfilter throw_exception /; + +our $standard_rules = {}; + +=head1 METHODS + +=head2 new + + Create validator object. Loads standard rules only. + +=cut + +sub new { + my ($class) = @_; + my $self = bless { _errors => {}, _rules => {} }, $class; + return $self->option( reset => 1 )->load($standard_rules); +} + +=head2 load + + $validator->load( $rules ); + + Load rules. Overwrite existing ones. + +=cut + +sub load { + my ( $self, $rules ) = @_; + if ( ref($rules) eq 'HASH' ) { + $self->{_rules} = { %{ $self->{_rules} }, %$rules }; + } else { + Koha::Exceptions::BadParameter->throw( parameter => 'rules' ); + } + return $self->_replace_aliases; +} + +=head2 unload + + $validator->unload; + + Unload custom rules, reload standard rules. + +=cut + +sub unload { + my ($self) = @_; + $self->{_rules} = {}; + return $self->load($standard_rules); +} + +=head2 check + + $validator->check( [ 1, 2, 3 ] ); + $validator->check( { a => 1, b => 2, c => 3 } ); + + Apply loaded rules to a set of values (in arrayref or hashref). + Returns either validated results (ref) or undef. But if throw_exception + is set, a validation error will trigger an exception. + If replace_inline is set, the passed reference will be modified too. + +=cut + +sub check { + my ( $self, $values ) = @_; + $self->{_errors} = {}; + if ( ref($values) eq 'HASH' ) { + return $self->_check_hash($values); + } elsif ( ref($values) eq 'ARRAY' ) { + return $self->_check_list($values); + } + Koha::Exceptions::BadParameter->throw( parameter => 'values' ); +} + +=head2 option + + $value = $validator->option('some_option'); # get + $validator->option( reset => 1 ); # reset to default + $validator->option($hash); # set + $validator->option(%hash); # set + + Get or set validator options. + +=cut + +sub option { + my ( $self, @args ) = @_; + if ( @args == 1 ) { + if ( !ref( $args[0] ) ) { + return ( any { $_ eq $args[0] } OPTIONS ) ? $self->{ '_' . $args[0] } : undef; + } elsif ( ref( $args[0] ) eq 'HASH' ) { + return $self->_set_opts( $args[0] ); + } + } elsif ( @args == 2 && $args[0] eq 'reset' ) { # reset to default values + $self->{_allow_missing_rules} = 0; + $self->{_break_on_error} = 1; + $self->{_restrict_postfilter} = 1; + $self->{_replace_inline} = 0; + $self->{_throw_exception} = 1; + return $self; + } elsif ( @args % 2 == 0 ) { + return $self->_set_opts( {@args} ); + } + Koha::Exceptions::BadParameter->throw( parameter => [@args] ); +} + +=head2 errors + + $count = $validator->errors; + $code = $validator->errors('field_name'); + $hash = $validator->errors( hash => 1 ); # { field1 => code1, ... } + $list = $validator->errors( code => CODE ); # [ field1, ... ] + $list = $validator->errors( array | list => 1 ); # [ field1, ... ] + + Returns error information. + +=cut + +sub errors { + my ( $self, @args ) = @_; + if ( @args == 0 ) { + return scalar keys %{ $self->{_errors} }; + } elsif ( @args == 1 ) { + return $self->{_errors}->{ $args[0] }; + } elsif ( @args == 2 && $args[0] eq 'code' ) { + return [ grep { $self->{_errors}->{$_} eq $args[1] } sort keys %{ $self->{_errors} } ]; + } elsif ( @args == 2 && $args[0] eq 'hash' ) { + return { %{ $self->{_errors} } }; # clone + } elsif ( @args == 2 && ( $args[0] eq 'list' || $args[0] eq 'array' ) ) { + return [ sort keys %{ $self->{_errors} } ]; + } + Koha::Exceptions::BadParameter->throw( parameter => [@args] ); +} + +=head1 INTERNAL ROUTINES + +=cut + +sub _replace_aliases { + my $self = shift; + foreach my $key ( keys %{ $self->{_rules} } ) { + my $rule = $self->{_rules}->{$key}; + next if ref($rule) eq 'HASH'; + if ( $rule && ref( $self->{_rules}->{$rule} ) eq 'HASH' ) { + $self->{_rules}->{$key} = $self->{_rules}->{$rule}; + } else { + Koha::Exceptions::BadParameter->throw("Bad rule for $key"); + } + } + return $self; +} + +sub _set_opts { + my ( $self, $options ) = @_; + foreach my $key ( keys %$options ) { + next unless any { $_ eq $key } OPTIONS; # just ignores unknown options + $self->{"_$key"} = $options->{$key} ? 1 : 0; + } + return $self; +} + +sub _check_hash { + my ( $self, $values ) = @_; + + return unless $self->_check_mandatory($values) || !$self->{_break_on_error}; + + my $results = {}; + foreach my $key ( sort keys %$values ) { + my $rule = exists $self->{_rules}->{$key} ? $self->{_rules}->{$key} : $self->{_rules}->{defaultrule}; + my $value = $values->{$key}; + $results->{$key} = $self->_check_value( $rule, $results, $value, $key ); + last if exists $self->{_errors}->{$key} && $self->{_break_on_error}; + } + return $self->_handle_record_validation_error if scalar %{ $self->{_errors} }; + $self->_replace_hash( $results, $values ) if $self->{_replace_inline}; + return $results; +} + +sub _check_mandatory { + my ( $self, $values ) = @_; + foreach my $key ( keys %{ $self->{_rules} } ) { + next unless ref( $self->{_rules}->{$key} ) eq 'HASH' && $self->{_rules}->{$key}->{mandatory}; + if ( !exists $values->{$key} ) { + $self->_handle_validation_error( $key, undef, 'MANDATORY' ); + return if $self->{_break_on_error}; + } + } + return !scalar %{ $self->{_errors} }; +} + +sub _handle_record_validation_error { + my $self = shift; + + # coming here implies that break_on_error is false + if ( $self->{_throw_exception} ) { + Koha::Exceptions::BadParameter->throw( parameter => $self->errors( hash => 1 ) ); + } + return; +} + +sub _replace_hash { + my ( $self, $results, $values ) = @_; + map { $values->{$_} = $results->{$_} } keys %$results; +} + +sub _check_list { + my ( $self, $values ) = @_; + + my $index = 0; + my $results = []; + foreach my $value (@$values) { + my $rule = $self->{_rules}->{defaultrule}; + $results->[$index] = $self->_check_value( $rule, $results, $value, $index ); + last if exists $self->{_errors}->{$index} && $self->{_break_on_error}; + $index++; + } + return $self->_handle_record_validation_error if scalar %{ $self->{_errors} }; + $self->_replace_list( $results, $values ) if $self->{_replace_inline}; + return $results; +} + +sub _replace_list { + my ( $self, $results, $values ) = @_; + splice @$values, 0, scalar @$values, @$results; +} + +sub _check_value { + my ( $self, $rule, $results, $value, $index ) = @_; + + my ( $res, $code ); + + # Is rule defined, should it exist? + if ( defined($rule) ) { + $res = ref($rule) eq 'HASH' ? 1 : 0; + } else { + $res = $self->{_allow_missing_rules}; + $rule = {}; # prevent warns + } + $code = 'RULE' unless $res; + + # Check type of value? + if ( $res && exists $rule->{ref} ) { + my $type = $rule->{ref} // q{}; + $res = ref($type) eq q{} && ref($value) eq $type; + $code = 'REF' unless $res; + } + + # Insert defaultvalue? + if ( $res && exists $rule->{defaultvalue} ) { + $value = $rule->{defaultvalue} if !defined($value); + } + + if ( $res && exists $rule->{prefilter} ) { + $res = ref( $rule->{prefilter} ) eq 'CODE'; + try { $value = $rule->{prefilter}->( $value, $index ) } + catch { $res = 0 } + if $res; + $code = 'PREFILTER' unless $res; + } + + # Validation: values, regex and code + if ( $res && exists $rule->{values} ) { + $res = ref( $rule->{values} ) eq 'ARRAY' && any { $_ eq $value } @{ $rule->{values} }; + $code = 'VALUES' unless $res; + } + if ( $res && exists $rule->{regexp} ) { + $res = ref( $rule->{regexp} ) eq 'Regexp' && $value =~ $rule->{regexp}; + $code = 'REGEXP' unless $res; + } + if ( $res && exists $rule->{code} ) { + $res = ref( $rule->{code} ) eq 'CODE'; + try { $res = $rule->{code}->( $value, $index ) } + catch { $res = 0 } + if $res; + $code = 'CODE' unless $res; + } + + if ( $res && exists $rule->{postfilter} ) { + if ( $res = ref( $rule->{postfilter} ) eq 'CODE' ) { + my $data; + $data = ref($results) eq 'HASH' ? {%$results} : [@$results] if $self->{_restrict_postfilter}; #clone + try { $value = $rule->{postfilter}->( $value, $index, $data // $results ) } + catch { $res = 0 }; + } + $code = 'POSTFILTER' unless $res; + } + + $self->_handle_validation_error( $index, $value, $code ) unless $res; + return $value; +} + +sub _handle_validation_error { + my ( $self, $index, $value, $code ) = @_; + $self->{_errors}->{$index} = $code; + if ( $self->{_throw_exception} && $self->{_break_on_error} ) { + Koha::Exceptions::BadParameter->throw( parameter => [ $index, $value, $code ] ); + } +} + +1; diff --git a/t/Koha/Validator.t b/t/Koha/Validator.t new file mode 100755 index 0000000000..6bcc198ca0 --- /dev/null +++ b/t/Koha/Validator.t @@ -0,0 +1,379 @@ +#!/usr/bin/perl + +# Copyright 2023 Rijksmuseum +# +# 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 Data::Dumper qw( Dumper ); +use Test::More tests => 5; +use Test::Exception; +use Test::Warn; +use Try::Tiny; + +use Koha::Validator; +use Koha::Email; + +our $validator; + +subtest 'new' => sub { + plan tests => 2; + + $validator = Koha::Validator->new; + is( ref($validator), 'Koha::Validator', 'Object type' ); + is( $validator->option('throw_exception'), 1, 'Check if option has default value' ); +}; + +subtest 'option' => sub { + plan tests => 10; + + is( ref( $validator->option ), 'Koha::Validator', 'Called option without parameters' ); + is( $validator->option('a'), undef, 'Check unknown option' ); + is( $validator->option('break_on_error'), 1, 'Check option break_on_error' ); + is( $validator->option('replace_inline'), 0, 'Check option replace_inline' ); + + is( ref $validator->option( a => 1 ), 'Koha::Validator', 'Trying to set unknown option' ); + is( $validator->option('a'), undef, 'Check unknown option again' ); + + $validator->option( a => 1, b => 2, replace_inline => 3, REPLACE_INLINE => 0 ); # last uc option incorrect + is( $validator->option('replace_inline'), 1, 'Check option replace_inline after setting with hash' ); + $validator->option( { a => 1, b => 2, break_on_error => 0, replace_inline => undef } ); + is( $validator->option('replace_inline'), 0, 'Check option replace_inline after setting with hashref' ); + is( $validator->option( reset => 1 )->option('break_on_error'), 1, 'break_on_error has been reset' ); + throws_ok { $validator->option( a => 1, b => 2, 3 ) } 'Koha::Exceptions::BadParameter', 'Bad number of parameters'; +}; + +subtest 'load' => sub { + plan tests => 15; + + throws_ok { $validator->load } 'Koha::Exceptions::BadParameter', + 'No input for load'; + throws_ok { $validator->load('wrong') } 'Koha::Exceptions::BadParameter', + 'Wrong input for load'; + throws_ok { $validator->load( { defaultrule => undef } ) } 'Koha::Exceptions::BadParameter', + 'Rule should not be undefined'; + throws_ok { $validator->load( { defaultrule => 'test' } ) } 'Koha::Exceptions::BadParameter', + 'The alias \'test\' cannot be resolved'; + + $validator->option( throw_exception => 0, allow_missing_rules => 1 ); + + is( ref $validator->load( { defaultrule => {} } ), 'Koha::Validator', 'Load returned object' ); + ok( $validator->check( { 1 => 2 } ), 'Checked hash with empty defaultrule' ); + ok( $validator->check( [ 1, 2 ] ), 'Checked array with empty defaultrule' ); + $validator->load( { defaultrule => { code => sub { 1 } } } ); + ok( $validator->check( { 1 => 2 } ), 'Checked hash with true sub' ); + ok( $validator->check( [ 1, 2 ] ), 'Checked array with true sub' ); + $validator->load( { defaultrule => { code => sub { } } } ); + ok( !$validator->check( { 1 => 2 } ), 'Checked hash with false sub' ); + ok( !$validator->check( [ 1, 2 ] ), 'Checked array with false sub' ); + + $validator->unload->load( { my_column => { values => [ 2, 3 ] } } ); + ok( $validator->check( [1] ), 'Checked array without defaultrule' ); + ok( $validator->check( { my_column => 3 } ), 'Checked hash with single rule' ); + + # Add standard rules, overwrite + $Koha::Validator::standard_rules = { rule1 => { regexp => qr/\D/ }, rule2 => { defaultvalue => 1 } }; + ok( $validator->unload->check( { rule1 => '2a' } ), 'Standard rule applied' ); + $validator->load( { rule1 => { postfilter => sub { 2 } } } ); + is_deeply( + $validator->check( { rule1 => '2a', rule2 => undef } ), { rule1 => 2, rule2 => 1 }, + 'Rule1 overwritten' + ); + $Koha::Validator::standard_rules = {}; + + $validator->unload->option( reset => 1 ); +}; + +subtest 'check' => sub { + plan tests => 19; + + throws_ok { $validator->check } 'Koha::Exceptions::BadParameter', 'No values to check'; + throws_ok { $validator->check(1) } 'Koha::Exceptions::BadParameter', 'Check does only accept hashref or arrayref'; + ok( $validator->check( {} ), 'Empty hash validated' ); + ok( $validator->check( [] ), 'Empty list validated' ); + + subtest 'arrayref' => sub { + $validator->load( { defaultrule => { values => [ 1, 2, 3 ] } } )->option( throw_exception => 0 ); + ok( $validator->check( [ 1, 2, '2', 3, '1' ] ), 'Check array of integers and characters with defaultrule' ); + + my $code = sub { + my ( $value, $index ) = @_; + return ( $value == 2 or $value == 3 ) if $index == 0; + return $value == 2 if $index == 1; + return $value =~ qr/test/ if $index == 2; + return 0 if $index == 3; + return 0; + }; + $validator->load( { defaultrule => { ref => '', code => $code } } ); + + ok( !$validator->check( [ 1, ] ), 'Checked array with one element' ); + is( $validator->errors(0), 'CODE', 'Check error' ); + ok( $validator->check( [ 3, ] ), 'Checked another array with one element' ); + ok( !$validator->check( [ [1] ] ), 'Checked another array with one nested arrayref' ); + ok( !$validator->check( [ 2, 3 ] ), 'Checked array with two elements, fallback fails on index 1' ); + is( $validator->errors(1), 'CODE', 'Check error for two elements array' ); + ok( $validator->check( [ 2, '2', 'testcase' ] ), 'Checked array with three elements' ); + ok( !$validator->check( [ 2, '2', 'textcase' ] ), 'Checked another array with three elements' ); + is( $validator->errors(2), 'CODE', 'Check error for three elements array' ); + ok( !$validator->check( [ 2, '2', 'testcase', 'index3' ] ), 'Checked array with four elements' ); + is( $validator->errors(3), 'CODE', 'Check error for four elements array' ); + + $validator->unload->option( reset => 1 ); + }; + + subtest 'hashref' => sub { + my $rules = { + digits_only => { regexp => qr/^\d+$/ }, + email => { code => sub { Koha::Email->is_valid( $_[0] ) } }, + test01 => 'digits_only', + test02 => { values => [ 1, 2 ] }, + test03 => { code => sub { 0 } }, + test04 => 'email', + test05 => { regexp => qr/test/ }, + }; + $validator->load($rules); + my $data = { test01 => 2, test02 => 1, test04 => 'marcel@test.nl', test05 => 'includes test word' }; + try { $validator->check($data); ok( 1, 'validator green' ) } + catch { ok(0) }; + is( $validator->errors, 0, 'No errors encountered' ); + + $data = { test01 => 2, test02 => 1, test03 => 'nomatterwhat' }; + try { $validator->check($data); ok(0) } + catch { ok( 1, 'validator red' ) }; + is( $validator->errors('test03'), 'CODE', 'Check error code for test03' ); + + my $code = sub { return Koha::Email->is_valid( $_[0] ) && $_[0] !~ /[\`]/ }; + $validator->load( { defaultrule => 'email', email => { code => $code } } ); + $data = { email => 'withbacktick`ls`@test.com' }; + try { $validator->check($data); ok(0) } + catch { is( $validator->errors('email'), 'CODE', 'validator red for email with backticks' ) }; + ok( $validator->check( { emailpro => 'john@doe.com' } ), 'John Doe never fails' ); + $validator->unload->option( reset => 1 ); + }; + + subtest 'mandatory' => sub { + $validator->option( throw_exception => 0 ); + $validator->load( { test => { mandatory => 1 }, test2 => {}, test3 => { mandatory => 0 } } ); + ok( !$validator->check( {} ), 'Missing test column' ); + is( $validator->errors('test'), 'MANDATORY', 'Check field' ); + ok( $validator->check( { test => 1, test2 => 0 } ), 'only test should be present' ); + $validator->unload->option( reset => 1 ); + }; + + subtest 'ref' => sub { + $validator->option( break_on_error => 0, throw_exception => 0 ); + $validator->load( { a => { ref => q{} }, b => { ref => 'HASH' }, c => {}, d => { ref => [1] } } ); + ok( !$validator->check( { a => [], b => q{}, c => 1 } ), 'a and b should fail' ); + is_deeply( $validator->errors( code => 'REF' ), [ 'a', 'b' ], 'Check wrong fields' ); + ok( !$validator->check( { d => 1 } ), 'Wrong ref definition, scalar expected' ); + $validator->unload->option( reset => 1 ); + }; + + subtest 'defaultvalue' => sub { + $validator->load( { defaultrule => { defaultvalue => 1 } } ); + is_deeply( $validator->check( [ undef, q{}, 0, 2 ] ), [ 1, q{}, 0, 2 ], 'defaultvalue on array' ); + is_deeply( $validator->check( { a => undef, b => q{} } ), { a => 1, b => q{} }, 'defaultvalue on hash' ); + $validator->unload; + }; + + subtest 'prefilter' => sub { + $validator->load( { test => { prefilter => sub { $_[0] =~ s/\s//g; $_[0] } } } ); + is_deeply( $validator->check( { test => 'a b c ' } ), { test => 'abc' }, 'Prefilter removed spaces' ); + $validator->load( { test => { prefilter => undef } } ); + throws_ok { $validator->check( { test => 'a b c ' } ) } 'Koha::Exceptions::BadParameter', + 'Wrong prefilter, no coderef'; + $validator->unload; + }; + + subtest 'values' => sub { + $validator->option( break_on_error => 0, throw_exception => 0 ); + + # Note: values for a and b are bad definitions! + $validator->load( { a => { values => undef }, b => { values => 1 }, c => { values => [ 10, 20 ] } } ); + $validator->check( { a => 1, b => 1, c => 1 } ); + is_deeply( $validator->errors( code => 'VALUES' ), [ 'a', 'b', 'c' ], 'a, b and c failed' ); + $validator->unload; + $validator->load( { defaultrule => { values => [ 1, 2 ] }, c => { values => [3] } } ); + ok( $validator->check( { a => 1, b => 2, c => 3 } ), 'All passed now' ); + + $validator->unload->option( reset => 1 ); + }; + + subtest 'regexp' => sub { + $validator->option( break_on_error => 0 ); + $validator->load( { defaultrule => { regexp => '/test/i' } } ); # bad defined + try { $validator->check( [1] ); ok(0) } + catch { ok( 1, 'Bad regex definition' ) }; + $validator->load( { defaultrule => { regexp => qr/\d\D/ } } ); + try { $validator->check( [ '123', '1b', 'ab2' ] ); ok(0) } + catch { ok( 1, 'Should fail on some elements' ) }; + is_deeply( $validator->errors( code => 'REGEXP' ), [ 0, 2 ], 'Two elements failed' ); + $validator->unload->option( reset => 1 ); + }; + + subtest 'code' => sub { + $validator->load( { test1 => { code => undef }, test2 => { code => 1 } } ); # bad defined + try { $validator->check( { test1 => 1 } ); ok(0) } + catch { ok( 1, 'Should fail on code undef' ) }; + try { $validator->check( { test2 => 2 } ); ok(0) } + catch { ok( 1, 'Should fail on code == 1' ) }; + $validator->load( + { defaultrule => { code => sub { $_[0] =~ /a/ } }, test2 => { code => sub { $_[0] =~ /b/ } } } ); + my $res = $validator->check( { test => 'Aa', test2 => 'bB' } ); + $validator->unload; + }; + + subtest 'postfilter' => sub { + $validator->option( throw_exception => 0 ); + $validator->load( + { + defaultrule => { + prefilter => sub { $_[0] =~ s/\s//g; $_[0] }, + postfilter => sub { uc $_[0] }, + }, + test => {}, + test3 => { + postfilter => sub { die }, + }, + } + ); + my $results = $validator->check( { test => 'a b c ', test2 => 'd e' } ); + is( $results->{test}, 'a b c ', 'Field \'test\' does not hit postfilter' ); + is( $results->{test2}, 'DE', 'Test2 passed postfilter of defaultrule' ); + + # what if postfilter crashes + ok( !$validator->check( { test => 'a b c ', test2 => 'd e', test3 => 'f g' } ), 'Should stumble over die' ); + is( $validator->errors('test3'), 'POSTFILTER', 'Check which field did not validate' ); + + $validator->unload->option( reset => 1 ); + }; + + subtest 'allow_missing_rules' => sub { + $validator->option( allow_missing_rules => 1, throw_exception => 0 ); + $validator->load( { test => { regexp => qr/\d/ } } ); + ok( $validator->check( { test => 2 } ), 'Validated' ); + ok( $validator->check( { tezt => 3 } ), 'No rule for tezt is allowed here' ); + $validator->option( allow_missing_rules => 0 ); + ok( !$validator->check( { tezt => 3 } ), 'No rule for tezt no longer allowed' ); + $validator->unload->option( reset => 1 ); + }; + + subtest 'break_on_error' => sub { + $validator->option( throw_exception => 0 ); + $validator->load( { a => { values => [ 1, 2 ] }, b => { values => [ 2, 3 ] } } ); + ok( !$validator->check( { a => 0, b => 0 } ), 'Wrong values for a and b' ); + is_deeply( $validator->errors( array => 1 ), ['a'], 'Stopped when a was wrong' ); + $validator->option( break_on_error => 0 ); + ok( !$validator->check( { a => 0, b => 0 } ), 'Check a and b again' ); + is_deeply( $validator->errors( array => 1 ), [ 'a', 'b' ], 'Found a and b now' ); + $validator->unload->option( reset => 1 ); + }; + + subtest 'replace_inline' => sub { + $validator->option( replace_inline => 0, throw_exception => 0 ); + $validator->load( { defaultrule => { ref => q{}, postfilter => sub { $_[0] + 1 } } } ); + + my $array = [ 1, 2 ]; + my $hash = { a => 1, b => 2 }; + is_deeply( $validator->check($array), [ 2, 3 ], 'Check results array' ); + is_deeply( $validator->check($hash), { a => 2, b => 3 }, 'Check results hash' ); + is_deeply( $array, [ 1, 2 ], 'Original array untouched' ); + is_deeply( $hash, { a => 1, b => 2 }, 'Original hash untouched' ); + + $validator->option( replace_inline => 1 ); + + # First, add an invalid argument (reference); check and test if not modified + $array = [ 1, 2, {} ]; + $hash = { a => 1, b => 2, c => [] }; + ok( !$validator->check($array), 'Array does no longer validate' ); + ok( !$validator->check($hash), 'Hash does no longer validate' ); + is_deeply( $array, [ 1, 2, {} ], 'Original array untouched' ); + is_deeply( $hash, { a => 1, b => 2, c => [] }, 'Original hash untouched' ); + + # Pass valid content, and test if modified + $array = [ 1, 2 ]; + $hash = { a => 1, b => 2 }; + $validator->check($array); + $validator->check($hash); + is_deeply( $array, [ 2, 3 ], 'Original array modified' ); + is_deeply( $hash, { a => 2, b => 3 }, 'Original hash modified' ); + + $validator->unload->option( reset => 1 ); + }; + + subtest 'restrict_postfilter' => sub { + # Example of 3 fields, a == b + c; note that validations starts with a.. + # Postfilter can only validate by throwing an exception. + my $code = sub { my ( $val, $idx, $res ) = @_; die unless $res->{a} == $res->{b} + $val; $val }; + $validator->load( { a => {}, b => {}, c => { postfilter => $code } } ); + ok( $validator->check( { a => 3, b => 2, c => 1 } ), 'Passes' ); + try { $validator->check( { a => 4, b => 2, c => 1 } ); ok(0) } + catch { ok( 1, 'Should fail' ) }; + + # Now try to make postfilter correct/enforce the value of a (restrict_postfilter on) + $code = sub { my ( $val, $idx, $res ) = @_; $res->{a} = $res->{b} + $val; $val }; + $validator->load( { a => {}, b => {}, c => { postfilter => $code } } ); + my $res = $validator->check( { a => 4, b => 2, c => 1 } ); + is( $res->{a}, 4, 'Postfilter failed to adjust the value of a' ); + # Correct attempt: restrict_postfilter off + $validator->option( restrict_postfilter => 0 ); + $res = $validator->check( { a => 4, b => 2, c => 1 } ); + is( $res->{a}, 3, 'Postfilter did adjust the value of a, restrict_postfilter off' ); + + $validator->unload->option( reset => 1 ); + }; + + subtest 'throw_exception' => sub { + try { $validator->check( [1] ); ok(0) } + catch { ok( 1, 'Exception for array without defaultrule' ) }; + $validator->option( throw_exception => 0 ); + my $res; + lives_ok { $res = $validator->check( [1] ) } 'No exception for same check'; + is( $res, undef, 'Returned undef' ); + $validator->unload->option( reset => 1 ); + }; +}; + +subtest 'errors' => sub { + plan tests => 14; + $validator->option( throw_exception => 0 ); + $validator->load( + { + defaultrule => { ref => q{}, values => [ 1, 2 ], regexp => qr/[3-9]/ }, + test => { mandatory => 1, code => sub { } } + } + ); + ok( !$validator->check( [ 0, {}, 2 ] ), 'Three elements should fail' ); + is( $validator->errors, 1, 'Check error count, break on' ); + is( $validator->errors(0), 'VALUES', 'Check error of index 0, break on' ); + is_deeply( $validator->errors( hash => 1 ), { 0 => 'VALUES' }, 'Check hash of errors, break on' ); + is_deeply( $validator->errors( list => 1 ), [0], 'Check list of errors, break on' ); + is_deeply( $validator->errors( code => 'REF' ), [], 'Check list of errors for REF, break on' ); + is_deeply( $validator->errors( code => 'VALUES' ), [0], 'Check list of errors for VALUES, break on' ); + + $validator->option( break_on_error => 0 ); + ok( !$validator->check( [ 0, {}, 2 ] ), 'Three elements should fail again' ); + is( $validator->errors, 3, 'Check error count, break off' ); + is( $validator->errors(2), 'REGEXP', 'Check error of index 2, break off' ); + is_deeply( + $validator->errors( hash => 1 ), { 0 => 'VALUES', 1 => 'REF', 2 => 'REGEXP' }, + 'Check hash of errors, break off' + ); + is_deeply( $validator->errors( array => 1 ), [ 0, 1, 2 ], 'Check array of errors, break off' ); + is_deeply( $validator->errors( code => 'REF' ), [1], 'Check list of errors for REF, break off' ); + is_deeply( $validator->errors( code => 'CODE' ), [], 'Check list of errors for CODE, break off' ); + $validator->unload->option( reset => 1 ); +}; -- 2.30.2