Bug 28786: Two-factor authentication for staff client - TOTP
[koha-ffzg.git] / t / lib / TestBuilder.pm
index 1f3ab51..59d923e 100644 (file)
@@ -1,9 +1,22 @@
 package t::lib::TestBuilder;
 
 use Modern::Perl;
-use Koha::Database;
+
+use Koha::Database qw( schema );
+use C4::Biblio qw( AddBiblio );
+use Koha::Biblios qw( _type );
+use Koha::Items qw( _type );
+use Koha::DateUtils qw( dt_from_string );
+
+use Bytes::Random::Secure;
+use Carp qw( carp );
+use Module::Load qw( load );
 use String::Random;
 
+use constant {
+    SIZE_BARCODE => 20, # Not perfect but avoid to fetch the value when creating a new item
+};
+
 sub new {
     my ($class) = @_;
     my $self = {};
@@ -51,13 +64,56 @@ sub delete {
     return $rv;
 }
 
+sub build_object {
+    my ( $self, $params ) = @_;
+
+    my $class = $params->{class};
+    my $value = $params->{value};
+
+    if ( not defined $class ) {
+        carp "Missing class param";
+        return;
+    }
+
+    my @unknowns = grep( !/^(class|value)$/, keys %{ $params });
+    carp "Unknown parameter(s): ", join( ', ', @unknowns ) if scalar @unknowns;
+
+    load $class;
+    my $source = $class->_type;
+
+    my $hashref = $self->build({ source => $source, value => $value });
+    my $object;
+    if ( $class eq 'Koha::Old::Patrons' ) {
+        $object = $class->search({ borrowernumber => $hashref->{borrowernumber} })->next;
+    } elsif ( $class eq 'Koha::Statistics' ) {
+        $object = $class->search({ datetime => $hashref->{datetime} })->next;
+    } else {
+        my @ids;
+        my @pks = $self->schema->source( $class->_type )->primary_columns;
+        foreach my $pk ( @pks ) {
+            push @ids, $hashref->{ $pk };
+        }
+
+        $object = $class->find( @ids );
+    }
+
+    return $object;
+}
+
 sub build {
 # build returns a hash of column values for a created record, or undef
 # build does NOT update a record, or pass back values of an existing record
     my ($self, $params) = @_;
-    my $source  = $params->{source} || return;
+    my $source  = $params->{source};
+    if( !$source ) {
+        carp "Source parameter not specified!";
+        return;
+    }
     my $value   = $params->{value};
 
+    my @unknowns = grep( !/^(source|value)$/, keys %{ $params });
+    carp "Unknown parameter(s): ", join( ', ', @unknowns ) if scalar @unknowns;
+
     my $col_values = $self->_buildColumnValues({
         source  => $source,
         value   => $value,
@@ -86,6 +142,63 @@ sub build {
     });
 }
 
+sub build_sample_biblio {
+    my ( $self, $args ) = @_;
+
+    my $title  = $args->{title}  || 'Some boring read';
+    my $author = $args->{author} || 'Some boring author';
+    my $frameworkcode = $args->{frameworkcode} || '';
+    my $itemtype = $args->{itemtype}
+      || $self->build_object( { class => 'Koha::ItemTypes' } )->itemtype;
+
+    my $marcflavour = C4::Context->preference('marcflavour');
+
+    my $record = MARC::Record->new();
+    $record->encoding( 'UTF-8' );
+
+    my ( $tag, $subfield ) = $marcflavour eq 'UNIMARC' ? ( 200, 'a' ) : ( 245, 'a' );
+    $record->append_fields(
+        MARC::Field->new( $tag, ' ', ' ', $subfield => $title ),
+    );
+
+    ( $tag, $subfield ) = $marcflavour eq 'UNIMARC' ? ( 200, 'f' ) : ( 100, 'a' );
+    $record->append_fields(
+        MARC::Field->new( $tag, ' ', ' ', $subfield => $author ),
+    );
+
+    ( $tag, $subfield ) = $marcflavour eq 'UNIMARC' ? ( 995, 'r' ) : ( 942, 'c' );
+    $record->append_fields(
+        MARC::Field->new( $tag, ' ', ' ', $subfield => $itemtype )
+    );
+
+    my ($biblio_id) = C4::Biblio::AddBiblio( $record, $frameworkcode );
+    return Koha::Biblios->find($biblio_id);
+}
+
+sub build_sample_item {
+    my ( $self, $args ) = @_;
+
+    my $biblionumber =
+      delete $args->{biblionumber} || $self->build_sample_biblio->biblionumber;
+    my $library = delete $args->{library}
+      || $self->build_object( { class => 'Koha::Libraries' } )->branchcode;
+
+    # If itype is not passed it will be picked from the biblio (see Koha::Item->store)
+
+    my $barcode = delete $args->{barcode}
+      || $self->_gen_text( { info => { size => SIZE_BARCODE } } );
+
+    return Koha::Item->new(
+        {
+            biblionumber  => $biblionumber,
+            homebranch    => $library,
+            holdingbranch => $library,
+            barcode       => $barcode,
+            %$args,
+        }
+    )->store->get_from_storage;
+}
+
 # ------------------------------------------------------------------------------
 # Internal helper routines
 
@@ -117,8 +230,15 @@ sub _create_links {
     if( $cnt_scalar == @$keys ) {
         # if one or more fk cols are null, the FK constraint will not be forced
         return {} if $cnt_null > 0;
+
         # does the record exist already?
-        return {} if $self->schema->resultset($linked_tbl)->find( $fk_value );
+        my @pks = $self->schema->source( $linked_tbl )->primary_columns;
+        my %fk_pk_value;
+        for (@pks) {
+            $fk_pk_value{$_} = $fk_value->{$_} if defined $fk_value->{$_};
+        }
+        return {} if !(keys %fk_pk_value);
+        return {} if $self->schema->resultset($linked_tbl)->find( \%fk_pk_value );
     }
     # create record with a recursive build call
     my $row = $self->build({ source => $linked_tbl, value => $fk_value });
@@ -150,8 +270,8 @@ sub _buildColumnValues {
     my @columns = $self->schema->source($source)->columns;
     my %unique_constraints = $self->schema->source($source)->unique_constraints();
 
-    my $build_value = 3;
-    # we try max three times if there are unique constraints
+    my $build_value = 5;
+    # we try max $build_value times if there are unique constraints
     BUILD_VALUE: while ( $build_value ) {
         # generate random values for all columns
         for my $col_name( @columns ) {
@@ -292,7 +412,9 @@ sub _buildColumnValue {
         }
         push @$retvalue, $value->{$col_name};
     } elsif( exists $self->{default_values}{$source}{$col_name} ) {
-        push @$retvalue, $self->{default_values}{$source}{$col_name};
+        my $v = $self->{default_values}{$source}{$col_name};
+        $v = &$v() if ref($v) eq 'CODE';
+        push @$retvalue, $v;
     } else {
         my $data_type = $col_info->{data_type};
         $data_type =~ s| |_|;
@@ -378,35 +500,40 @@ sub _gen_real {
     if( defined( $params->{info}->{size} ) ) {
         $max = 10 ** ($params->{info}->{size}->[0] - $params->{info}->{size}->[1]);
     }
-    return rand($max) + 1;
+    $max = 10 ** 5 if $max > 10 ** 5;
+    return sprintf("%.2f", rand($max-0.1));
 }
 
 sub _gen_date {
     my ($self, $params) = @_;
-    return $self->schema->storage->datetime_parser->format_date(DateTime->now())
+    return $self->schema->storage->datetime_parser->format_date(dt_from_string)
 }
 
 sub _gen_datetime {
     my ($self, $params) = @_;
-    return $self->schema->storage->datetime_parser->format_datetime(DateTime->now());
+    return $self->schema->storage->datetime_parser->format_datetime(dt_from_string);
 }
 
 sub _gen_text {
     my ($self, $params) = @_;
     # From perldoc String::Random
-    # max: specify the maximum number of characters to return for * and other
-    # regular expression patters that don't return a fixed number of characters
-    my $regex = '[A-Za-z][A-Za-z0-9_]*';
-    my $size = $params->{info}{size};
-    if ( defined $size and $size > 1 ) {
-        $size--;
-    } elsif ( defined $size and $size == 1 ) {
-        $regex = '[A-Za-z]';
-    }
-    my $random = String::Random->new( max => $size );
+    my $size = $params->{info}{size} // 10;
+    $size -= alt_rand(0.5 * $size);
+    my $regex = $size > 1
+        ? '[A-Za-z][A-Za-z0-9_]{'.($size-1).'}'
+        : '[A-Za-z]';
+    my $random = String::Random->new( rand_gen => \&alt_rand );
+    # rand_gen is only supported from 0.27 onward
     return $random->randregex($regex);
 }
 
+sub alt_rand { #Alternative randomizer
+    my ($max) = @_;
+    my $random = Bytes::Random::Secure->new( NonBlocking => 1 );
+    my $r = $random->irand / 2**32;
+    return int( $r * $max );
+}
+
 sub _gen_set_enum {
     my ($self, $params) = @_;
     return $params->{info}->{extra}->{list}->[0];
@@ -420,9 +547,63 @@ sub _gen_blob {
 sub _gen_default_values {
     my ($self) = @_;
     return {
+        Borrower => {
+            login_attempts => 0,
+            gonenoaddress  => undef,
+            lost           => undef,
+            debarred       => undef,
+            borrowernotes  => '',
+            secret         => undef,
+        },
         Item => {
+            notforloan         => 0,
+            itemlost           => 0,
+            withdrawn          => 0,
+            restricted         => 0,
+            damaged            => 0,
+            materials          => undef,
             more_subfields_xml => undef,
         },
+        Category => {
+            enrolmentfee => 0,
+            reservefee   => 0,
+            # Not X, used for statistics
+            category_type => sub { return [ qw( A C S I P ) ]->[int(rand(5))] },
+            min_password_length => undef,
+            require_strong_password => undef,
+        },
+        Branch => {
+            pickup_location => 0,
+        },
+        Reserve => {
+            non_priority => 0,
+        },
+        Itemtype => {
+            rentalcharge => 0,
+            rentalcharge_daily => 0,
+            rentalcharge_hourly => 0,
+            defaultreplacecost => 0,
+            processfee => 0,
+            notforloan => 0,
+        },
+        Aqbookseller => {
+            tax_rate => 0,
+            discount => 0,
+        },
+        AuthHeader => {
+            marcxml => '',
+        },
+        BorrowerAttributeType => {
+            mandatory => 0,
+        },
+        Suggestion => {
+            suggesteddate => dt_from_string()->ymd,
+            STATUS        => 'ASKED'
+        },
+        ReturnClaim => {
+            issue_id => undef, # It should be a FK but we removed it
+                               # We don't want to generate a random value
+        },
     };
 }
 
@@ -501,6 +682,13 @@ Note that you should wrap these actions in a transaction yourself.
     Realize that passing primary key values to build may result in undef
     if a record with that primary key already exists.
 
+=head2 build_object
+
+Given a plural Koha::Object-derived class, it creates a random element, and
+returns the corresponding Koha::Object.
+
+    my $patron = $builder->build_object({ class => 'Koha::Patrons' [, value => { ... }] });
+
 =head1 AUTHOR
 
 Yohann Dufour <yohann.dufour@biblibre.com>